diff --git a/CONTRIBUTORS b/CONTRIBUTORS index dd235666..4752f566 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -7,3 +7,7 @@ Cornel - cornelk Łukasz Kurowski - crackcomm Adrian Gallagher - thrasher- Manuel Kreutz - 140am +libsora.so - if1live +Tong - tongxiaofeng +Jamie Cheng - starit +Jake - snipesjr \ No newline at end of file diff --git a/README.md b/README.md index 9841947f..4c48fe1f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | Bitstamp | Yes | Yes | NA | | Bittrex | Yes | No | NA | | BTCC | Yes | Yes | No | -| BTCE | Yes | NA | NA | | BTCMarkets | Yes | NA | NA | | COINUT | Yes | No | NA | | GDAX(Coinbase) | Yes | Yes | No| @@ -37,6 +36,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | LocalBitcoins | Yes | NA | NA | | OKCoin (both) | Yes | Yes | No | | Poloniex | Yes | Yes | NA | +| WEX | Yes | NA | NA | We are aiming to support the top 20 highest volume exchanges based off the [CoinMarketCap exchange data](https://coinmarketcap.com/exchanges/volume/24-hour/). diff --git a/common/common.go b/common/common.go index 0bf1791e..2717e727 100644 --- a/common/common.go +++ b/common/common.go @@ -20,6 +20,7 @@ import ( "net/http" "net/url" "os" + "reflect" "regexp" "strconv" "strings" @@ -294,15 +295,18 @@ func SendHTTPRequest(method, path string, headers map[string]string, body io.Rea // SendHTTPGetRequest sends a simple get request using a url string & JSON // decodes the response into a struct pointer you have supplied. Returns an error // on failure. -func SendHTTPGetRequest(url string, jsonDecode bool, result interface{}) error { +func SendHTTPGetRequest(url string, jsonDecode, isVerbose bool, result interface{}) error { + if isVerbose { + log.Println("Raw URL: ", url) + } + res, err := http.Get(url) if err != nil { return err } if res.StatusCode != 200 { - log.Printf("HTTP status code: %d\n", res.StatusCode) - return errors.New("status code was not 200") + return fmt.Errorf("common.SendHTTPGetRequest() error: HTTP status code %d", res.StatusCode) } contents, err := ioutil.ReadAll(res.Body) @@ -310,16 +314,18 @@ func SendHTTPGetRequest(url string, jsonDecode bool, result interface{}) error { return err } + if isVerbose { + log.Println("Raw Resp: ", string(contents[:])) + } + defer res.Body.Close() if jsonDecode { - err := JSONDecode(contents, &result) + err := JSONDecode(contents, result) if err != nil { log.Println(string(contents[:])) return err } - } else { - result = &contents } return nil @@ -332,6 +338,9 @@ func JSONEncode(v interface{}) ([]byte, error) { // JSONDecode decodes JSON data into a structure func JSONDecode(data []byte, to interface{}) error { + if !StringContains(reflect.ValueOf(to).Type().String(), "*") { + return errors.New("json decode error - memory address not supplied") + } return json.Unmarshal(data, to) } diff --git a/common/common_test.go b/common/common_test.go index 806c9d85..583b7025 100644 --- a/common/common_test.go +++ b/common/common_test.go @@ -209,6 +209,11 @@ func TestBase64Decode(t *testing.T) { expectedOutput, actualResult, err), ) } + + _, err = Base64Decode("-") + if err == nil { + t.Error("Test failed. Bad base64 string failed returned nil error") + } } func TestBase64Encode(t *testing.T) { @@ -226,7 +231,7 @@ func TestBase64Encode(t *testing.T) { func TestStringSliceDifference(t *testing.T) { t.Parallel() originalInputOne := []string{"hello"} - originalInputTwo := []string{"moto"} + originalInputTwo := []string{"hello", "moto"} expectedOutput := []string{"hello moto"} actualResult := StringSliceDifference(originalInputOne, originalInputTwo) if reflect.DeepEqual(expectedOutput, actualResult) { @@ -334,14 +339,17 @@ func TestReplaceString(t *testing.T) { func TestRoundFloat(t *testing.T) { t.Parallel() - originalInput := float64(1.4545445445) - precisionInput := 2 - expectedOutput := float64(1.45) - actualResult := RoundFloat(originalInput, precisionInput) - if expectedOutput != actualResult { - t.Error(fmt.Sprintf( - "Test failed. Expected '%f'. Actual '%f'.", expectedOutput, actualResult), - ) + // mapping of input vs expected result + testTable := map[float64]float64{ + 2.3232323: 2.32, + -2.3232323: -2.32, + } + for testInput, expectedOutput := range testTable { + actualOutput := RoundFloat(testInput, 2) + if actualOutput != expectedOutput { + t.Error(fmt.Sprintf("Test failed. RoundFloat Expected '%f'. Actual '%f'.", + expectedOutput, actualOutput)) + } } } @@ -435,28 +443,28 @@ func TestSendHTTPRequest(t *testing.T) { headers["Content-Type"] = "application/x-www-form-urlencoded" _, err := SendHTTPRequest( - methodGarbage, "http://query.yahooapis.com/v1/public/yql", headers, + methodGarbage, "https://query.yahooapis.com/v1/public/yql", headers, strings.NewReader(""), ) if err == nil { t.Error("Test failed. ") } _, err = SendHTTPRequest( - methodPost, "http://query.yahooapis.com/v1/public/yql", headers, + methodPost, "https://query.yahooapis.com/v1/public/yql", headers, strings.NewReader(""), ) if err != nil { t.Errorf("Test failed. %s ", err) } _, err = SendHTTPRequest( - methodGet, "http://query.yahooapis.com/v1/public/yql", headers, + methodGet, "https://query.yahooapis.com/v1/public/yql", headers, strings.NewReader(""), ) if err != nil { t.Errorf("Test failed. %s ", err) } _, err = SendHTTPRequest( - methodDelete, "http://query.yahooapis.com/v1/public/yql", headers, + methodDelete, "https://query.yahooapis.com/v1/public/yql", headers, strings.NewReader(""), ) if err != nil { @@ -480,15 +488,15 @@ func TestSendHTTPGetRequest(t *testing.T) { url := `https://etherchain.org/api/account/multiple/0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe` result := test{} - err := SendHTTPGetRequest(url, true, &result) + err := SendHTTPGetRequest(url, true, false, &result) if err != nil { t.Errorf("Test failed - common SendHTTPGetRequest error: %s", err) } - err = SendHTTPGetRequest("DINGDONG", true, &result) + err = SendHTTPGetRequest("DINGDONG", true, false, &result) if err == nil { t.Error("Test failed - common SendHTTPGetRequest error") } - err = SendHTTPGetRequest(url, false, &result) + err = SendHTTPGetRequest(url, false, false, &result) if err != nil { t.Error("Test failed - common SendHTTPGetRequest error") } @@ -524,8 +532,8 @@ func TestJSONEncode(t *testing.T) { } func TestEncodeURLValues(t *testing.T) { - urlstring := "http://www.test.com" - expectedOutput := `http://www.test.com?env=TEST%2FDATABASE&format=json&q=SELECT+%2A+from+yahoo.finance.xchange+WHERE+pair+in+%28%22BTC%2CUSD%22%29` + urlstring := "https://www.test.com" + expectedOutput := `https://www.test.com?env=TEST%2FDATABASE&format=json&q=SELECT+%2A+from+yahoo.finance.xchange+WHERE+pair+in+%28%22BTC%2CUSD%22%29` values := url.Values{} values.Set("q", fmt.Sprintf( "SELECT * from yahoo.finance.xchange WHERE pair in (\"%s\")", "BTC,USD"), @@ -650,6 +658,11 @@ func TestWriteFile(t *testing.T) { if err != nil { t.Errorf("Test failed. Common WriteFile error: %s", err) } + + err = WriteFile("", nil) + if err == nil { + t.Error("Test failed. Common WriteFile allowed bad path") + } } func TestRemoveFile(t *testing.T) { @@ -672,9 +685,9 @@ func TestGetURIPath(t *testing.T) { t.Parallel() // mapping of input vs expected result testTable := map[string]string{ - "https://api.gdax.com/accounts": "/accounts", - "https://api.gdax.com/accounts?a=1&b=2": "/accounts?a=1&b=2", - "ht:tp:/invalidurl": "", + "https://api.gdax.com/accounts": "/accounts", + "https://api.gdax.com/accounts?a=1&b=2": "/accounts?a=1&b=2", + "http://www.google.com/accounts?!@#$%;^^": "", } for testInput, expectedOutput := range testTable { actualOutput := GetURIPath(testInput) diff --git a/config/config.go b/config/config.go index 1409efbe..5a156b51 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency" "github.com/thrasher-/gocryptotrader/portfolio" + "github.com/thrasher-/gocryptotrader/smsglobal" ) // Constants declared here are filename strings and test strings @@ -45,16 +46,19 @@ var ( WarningWebserverListenAddressInvalid = "WARNING -- Webserver support disabled due to invalid listen address." WarningWebserverRootWebFolderNotFound = "WARNING -- Webserver support disabled due to missing web folder." WarningExchangeAuthAPIDefaultOrEmptyValues = "WARNING -- Exchange %s: Authenticated API support disabled due to default/empty APIKey/Secret/ClientID values." + WarningCurrencyExchangeProvider = "WARNING -- Currency exchange provider invalid valid. Reset to Fixer." RenamingConfigFile = "Renaming config file %s to %s." Cfg Config ) // WebserverConfig struct holds the prestart variables for the webserver. type WebserverConfig struct { - Enabled bool - AdminUsername string - AdminPassword string - ListenAddress string + Enabled bool + AdminUsername string + AdminPassword string + ListenAddress string + WebsocketConnectionLimit int + WebsocketAllowInsecureOrigin bool } // SMSGlobalConfig structure holds all the variables you need for instant @@ -63,11 +67,7 @@ type SMSGlobalConfig struct { Enabled bool Username string Password string - Contacts []struct { - Name string - Number string - Enabled bool - } + Contacts []smsglobal.Contact } // Post holds the bot configuration data @@ -78,36 +78,43 @@ type Post struct { // CurrencyPairFormatConfig stores the users preferred currency pair display type CurrencyPairFormatConfig struct { Uppercase bool - Delimiter string + Delimiter string `json:",omitempty"` + Separator string `json:",omitempty"` + Index string `json:",omitempty"` } // Config is the overarching object that holds all the information for // prestart management of portfolio, SMSGlobal, webserver and enabled exchange type Config struct { - Name string - EncryptConfig int - Cryptocurrencies string - CurrencyPairFormat *CurrencyPairFormatConfig `json:"CurrencyPairFormat"` - Portfolio portfolio.Base `json:"PortfolioAddresses"` - SMS SMSGlobalConfig `json:"SMSGlobal"` - Webserver WebserverConfig `json:"Webserver"` - Exchanges []ExchangeConfig `json:"Exchanges"` + Name string + EncryptConfig int + Cryptocurrencies string + CurrencyExchangeProvider string + CurrencyPairFormat *CurrencyPairFormatConfig `json:"CurrencyPairFormat"` + FiatDisplayCurrency string + Portfolio portfolio.Base `json:"PortfolioAddresses"` + SMS SMSGlobalConfig `json:"SMSGlobal"` + Webserver WebserverConfig `json:"Webserver"` + Exchanges []ExchangeConfig `json:"Exchanges"` } // ExchangeConfig holds all the information needed for each enabled Exchange. type ExchangeConfig struct { - Name string - Enabled bool - Verbose bool - Websocket bool - RESTPollingDelay time.Duration - AuthenticatedAPISupport bool - APIKey string - APISecret string - ClientID string `json:",omitempty"` - AvailablePairs string - EnabledPairs string - BaseCurrencies string + Name string + Enabled bool + Verbose bool + Websocket bool + RESTPollingDelay time.Duration + AuthenticatedAPISupport bool + APIKey string + APISecret string + ClientID string `json:",omitempty"` + AvailablePairs string + EnabledPairs string + BaseCurrencies string + AssetTypes string + ConfigCurrencyPairFormat *CurrencyPairFormatConfig `json:"ConfigCurrencyPairFormat"` + RequestCurrencyPairFormat *CurrencyPairFormatConfig `json:"RequestCurrencyPairFormat"` } // GetConfigEnabledExchanges returns the number of exchanges that are enabled. @@ -232,6 +239,11 @@ func (c *Config) CheckWebserverConfigValues() error { if port < 1 || port > 65355 { return errors.New(WarningWebserverListenAddressInvalid) } + + if c.Webserver.WebsocketConnectionLimit <= 0 { + c.Webserver.WebsocketConnectionLimit = 1 + } + return nil } @@ -408,6 +420,31 @@ func (c *Config) LoadConfig(configPath string) error { return fmt.Errorf(ErrCheckingConfigValues, err) } + if c.SMS.Enabled { + err = c.CheckSMSGlobalConfigValues() + if err != nil { + log.Print(fmt.Errorf(ErrCheckingConfigValues, err)) + c.SMS.Enabled = false + } + } + + if c.Webserver.Enabled { + err = c.CheckWebserverConfigValues() + if err != nil { + log.Print(fmt.Errorf(ErrCheckingConfigValues, err)) + c.Webserver.Enabled = false + } + } + + if c.CurrencyExchangeProvider == "" { + c.CurrencyExchangeProvider = "fixer" + } else { + if c.CurrencyExchangeProvider != "yahoo" && c.CurrencyExchangeProvider != "fixer" { + log.Println(WarningCurrencyExchangeProvider) + c.CurrencyExchangeProvider = "fixer" + } + } + if c.CurrencyPairFormat == nil { c.CurrencyPairFormat = &CurrencyPairFormatConfig{ Delimiter: "-", @@ -415,6 +452,47 @@ func (c *Config) LoadConfig(configPath string) error { } } + if c.FiatDisplayCurrency == "" { + c.FiatDisplayCurrency = "USD" + } + + return nil +} + +// UpdateConfig updates the config with a supplied config file +func (c *Config) UpdateConfig(configPath string, newCfg Config) error { + if c.Name != newCfg.Name && newCfg.Name != "" { + c.Name = newCfg.Name + } + + err := newCfg.CheckExchangeConfigValues() + if err != nil { + return err + } + c.Exchanges = newCfg.Exchanges + + if c.CurrencyPairFormat != newCfg.CurrencyPairFormat { + c.CurrencyPairFormat = newCfg.CurrencyPairFormat + } + + c.Portfolio = newCfg.Portfolio + + err = newCfg.CheckSMSGlobalConfigValues() + if err != nil { + return err + } + c.SMS = newCfg.SMS + + err = c.SaveConfig(configPath) + if err != nil { + return err + } + + err = c.LoadConfig(configPath) + if err != nil { + return err + } + return nil } diff --git a/config/config_test.go b/config/config_test.go index f31c5abd..3ca3aeac 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -5,7 +5,7 @@ import ( ) func TestGetConfigEnabledExchanges(t *testing.T) { - defaultEnabledExchanges := 17 + defaultEnabledExchanges := 18 GetConfigEnabledExchanges := GetConfig() err := GetConfigEnabledExchanges.LoadConfig(ConfigTestFile) if err != nil { diff --git a/config_example.dat b/config_example.dat index 65d01df0..baad385a 100644 --- a/config_example.dat +++ b/config_example.dat @@ -11,22 +11,26 @@ { "Address": "1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy", "CoinType": "BTC", - "Balance": 124178.0002442 + "Balance": 124178.00647714, + "Description": "" }, { "Address": "3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v", "CoinType": "BTC", - "Balance": 103439.83659727 + "Balance": 107843.84030984, + "Description": "" }, { "Address": "LgY8ahfHRhvjVQC1zJnBhFMG5pCTMuKRqh", "CoinType": "LTC", - "Balance": 3.00000005e+06 + "Balance": 100000.052, + "Description": "" }, { "Address": "0xb794f5ea0ba39494ce839613fffba74279579268", "CoinType": "ETH", - "Balance": 5.774999820458524e+06 + "Balance": 3.224999915984445e+24, + "Description": "" } ] }, @@ -58,10 +62,18 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", "AvailablePairs": "BTCUSD,BTCHKD,BTCEUR,BTCCAD,BTCAUD,BTCSGD,BTCJPY,BTCGBP,BTCNZD,LTCBTC,DOGEBTC,STRBTC,XRPBTC", "EnabledPairs": "BTCUSD,BTCHKD,BTCEUR,BTCCAD,BTCAUD,BTCSGD,BTCJPY,BTCGBP,BTCNZD,LTCBTC,DOGEBTC,STRBTC,XRPBTC", - "BaseCurrencies": "USD,HKD,EUR,CAD,AUD,SGD,JPY,GBP,NZD" + "BaseCurrencies": "USD,HKD,EUR,CAD,AUD,SGD,JPY,GBP,NZD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true, + "Index": "BTC" + }, + "RequestCurrencyPairFormat": { + "Uppercase": true, + "Index": "BTC" + } }, { "Name": "Bitfinex", @@ -72,10 +84,16 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", - "AvailablePairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC,ETCBTC,ETCUSD,BFXUSD,BFXBTC,RRTUSD,RRTBTC,ZECUSD,ZECBTC,XMRUSD,XMRBTC,DSHUSD,DSHBTC", + "AvailablePairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC,ETCBTC,ETCUSD,RRTUSD,RRTBTC,ZECUSD,ZECBTC,XMRUSD,XMRBTC,DSHUSD,DSHBTC,BCCBTC,BCUBTC,BCCUSD,BCUUSD,XRPUSD,XRPBTC,IOTUSD,IOTBTC,IOTETH,EOSUSD,EOSBTC,EOSETH,SANUSD,SANBTC,SANETH,OMGUSD,OMGBTC,OMGETH,BCHUSD,BCHBTC,BCHETH", "EnabledPairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC", - "BaseCurrencies": "USD" + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "Bitstamp", @@ -89,7 +107,14 @@ "ClientID": "ClientID", "AvailablePairs": "BTCUSD,BTCEUR,EURUSD,XRPUSD,XRPEUR", "EnabledPairs": "BTCUSD,BTCEUR,EURUSD,XRPUSD,XRPEUR", - "BaseCurrencies": "USD,EUR" + "BaseCurrencies": "USD,EUR", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "Bittrex", @@ -100,10 +125,18 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", - "AvailablePairs": "BTCLTC,BTCDOGE,BTCVTC,BTCPPC,BTCFTC,BTCRDD,BTCNXT,BTCDASH,BTCPOT,BTCBLK,BTCEMC2,BTCXMY,BTCAUR,BTCEFL,BTCGLD,BTCSLR,BTCPTC,BTCGRS,BTCNLG,BTCRBY,BTCXWC,BTCMONA,BTCTHC,BTCENRG,BTCERC,BTCNAUT,BTCVRC,BTCCURE,BTCXBB,BTCXMR,BTCCLOAK,BTCSTART,BTCKORE,BTCXDN,BTCTRUST,BTCNAV,BTCXST,BTCBTCD,BTCVIA,BTCUNO,BTCPINK,BTCIOC,BTCCANN,BTCSYS,BTCNEOS,BTCDGB,BTCBURST,BTCEXCL,BTCSWIFT,BTCDOPE,BTCBLOCK,BTCABY,BTCBYC,BTCXMG,BTCBLITZ,BTCBAY,BTCBTS,BTCFAIR,BTCSPR,BTCVTR,BTCXRP,BTCGAME,BTCCOVAL,BTCNXS,BTCXCP,BTCBITB,BTCGEO,BTCFLDC,BTCGRC,BTCFLO,BTCNBT,BTCMUE,BTCXEM,BTCCLAM,BTCDMD,BTCGAM,BTCSPHR,BTCOK,BTCSNRG,BTCPKB,BTCCPC,BTCAEON,BTCETH,BTCGCR,BTCTX,BTCBCY,BTCEXP,BTCINFX,BTCOMNI,BTCAMP,BTCAGRS,BTCXLM,BTCBTA,USDTBTC,BITCNYBTC,BTCCLUB,BTCVOX,BTCEMC,BTCFCT,BTCMAID,BTCEGC,BTCSLS,BTCRADS,BTCDCR,BTCSAFEX,BTCBSD,BTCXVG,BTCPIVX,BTCXVC,BTCMEME,BTCSTEEM,BTC2GIVE,BTCLSK,BTCPDC,BTCBRK,BTCDGD,ETHDGD,BTCWAVES,BTCRISE,BTCLBC,BTCSBD,BTCBRX,BTCDRACO,BTCETC,ETHETC,BTCSTRAT,BTCUNB,BTCSYNX,BTCTRIG,BTCEBST,BTCVRM,BTCSEQ,BTCXAUR,BTCSNGLS,BTCREP,BTCSHIFT,BTCARDR,BTCXZC,BTCNEO,BTCZEC,BTCZCL,BTCIOP,BTCDAR,BTCGOLOS,BTCHKG,BTCUBQ,BTCKMD,BTCGBG,BTCSIB,BTCION,BTCLMC,BTCQWARK,BTCCRW,BTCSWT,BTCTIME,BTCMLN,BTCARK,BTCDYN,BTCTKS,BTCMUSIC,BTCDTB,BTCINCNT,BTCGBYTE,BTCGNT,BTCNXC,BTCEDG,BTCLGD,BTCTRST,ETHGNT,ETHREP,USDTETH,ETHWINGS,BTCWINGS,BTCRLC,BTCGNO,BTCGUP,BTCLUN,ETHGUP,ETHRLC,ETHLUN,ETHSNGLS,ETHGNO,BTCAPX,BTCTKN,ETHTKN,BTCHMQ,ETHHMQ,BTCANT,ETHTRST,ETHANT,BTCSC,ETHBAT,BTCBAT,BTCZEN,BTC1ST,BTCQRL,ETH1ST,ETHQRL,BTCCRB,ETHCRB,ETHLGD,BTCPTOY,ETHPTOY,BTCMYST,ETHMYST,BTCCFI,ETHCFI,BTCBNT,ETHBNT,BTCNMR,ETHNMR,ETHTIME,ETHLTC,ETHXRP,BTCSNT,ETHSNT,BTCDCT,BTCXEL,BTCMCO,ETHMCO,BTCADT,ETHADT,BTCFUN,ETHFUN,BTCPAY,ETHPAY,BTCMTL,ETHMTL,BTCSTORJ,ETHSTORJ,BTCADX,ETHADX,ETHDASH,ETHSC,ETHZEC,USDTZEC,USDTLTC,USDTETC,USDTXRP,BTCOMG,ETHOMG,BTCCVC,ETHCVC,BTCPART,BTCQTUM,ETHQTUM,ETHXMR,ETHXEM,ETHXLM,ETHNEO,USDTXMR,USDTDASH,ETHBCC,USDTBCC,BTCBCC,USDTNEO,ETHWAVES,ETHSTRAT,ETHDGB,ETHFCT,ETHBTS", - "EnabledPairs": "BTCLTC,BTCDOGE,BTCDASH", - "BaseCurrencies": "USD" + "AvailablePairs": "BTC-LTC,BTC-DOGE,BTC-VTC,BTC-PPC,BTC-FTC,BTC-RDD,BTC-NXT,BTC-DASH,BTC-POT,BTC-BLK,BTC-EMC2,BTC-XMY,BTC-AUR,BTC-EFL,BTC-GLD,BTC-SLR,BTC-PTC,BTC-GRS,BTC-NLG,BTC-RBY,BTC-XWC,BTC-MONA,BTC-THC,BTC-ENRG,BTC-ERC,BTC-VRC,BTC-CURE,BTC-XBB,BTC-XMR,BTC-CLOAK,BTC-START,BTC-KORE,BTC-XDN,BTC-TRUST,BTC-NAV,BTC-XST,BTC-BTCD,BTC-VIA,BTC-UNO,BTC-PINK,BTC-IOC,BTC-CANN,BTC-SYS,BTC-NEOS,BTC-DGB,BTC-BURST,BTC-EXCL,BTC-SWIFT,BTC-DOPE,BTC-BLOCK,BTC-ABY,BTC-BYC,BTC-XMG,BTC-BLITZ,BTC-BAY,BTC-BTS,BTC-FAIR,BTC-SPR,BTC-VTR,BTC-XRP,BTC-GAME,BTC-COVAL,BTC-NXS,BTC-XCP,BTC-BITB,BTC-GEO,BTC-FLDC,BTC-GRC,BTC-FLO,BTC-NBT,BTC-MUE,BTC-XEM,BTC-CLAM,BTC-DMD,BTC-GAM,BTC-SPHR,BTC-OK,BTC-SNRG,BTC-PKB,BTC-CPC,BTC-AEON,BTC-ETH,BTC-GCR,BTC-TX,BTC-BCY,BTC-EXP,BTC-INFX,BTC-OMNI,BTC-AMP,BTC-AGRS,BTC-XLM,BTC-BTA,USDT-BTC,BTC-CLUB,BTC-VOX,BTC-EMC,BTC-FCT,BTC-MAID,BTC-EGC,BTC-SLS,BTC-RADS,BTC-DCR,BTC-SAFEX,BTC-BSD,BTC-XVG,BTC-PIVX,BTC-XVC,BTC-MEME,BTC-STEEM,BTC-2GIVE,BTC-LSK,BTC-PDC,BTC-BRK,BTC-DGD,ETH-DGD,BTC-WAVES,BTC-RISE,BTC-LBC,BTC-SBD,BTC-BRX,BTC-DRACO,BTC-ETC,ETH-ETC,BTC-STRAT,BTC-UNB,BTC-SYNX,BTC-TRIG,BTC-EBST,BTC-VRM,BTC-SEQ,BTC-XAUR,BTC-SNGLS,BTC-REP,BTC-SHIFT,BTC-ARDR,BTC-XZC,BTC-NEO,BTC-ZEC,BTC-ZCL,BTC-IOP,BTC-DAR,BTC-GOLOS,BTC-HKG,BTC-UBQ,BTC-KMD,BTC-GBG,BTC-SIB,BTC-ION,BTC-LMC,BTC-QWARK,BTC-CRW,BTC-SWT,BTC-TIME,BTC-MLN,BTC-ARK,BTC-DYN,BTC-TKS,BTC-MUSIC,BTC-DTB,BTC-INCNT,BTC-GBYTE,BTC-GNT,BTC-NXC,BTC-EDG,BTC-LGD,BTC-TRST,ETH-GNT,ETH-REP,USDT-ETH,ETH-WINGS,BTC-WINGS,BTC-RLC,BTC-GNO,BTC-GUP,BTC-LUN,ETH-GUP,ETH-RLC,ETH-LUN,ETH-SNGLS,ETH-GNO,BTC-APX,BTC-TKN,ETH-TKN,BTC-HMQ,ETH-HMQ,BTC-ANT,ETH-TRST,ETH-ANT,BTC-SC,ETH-BAT,BTC-BAT,BTC-ZEN,BTC-1ST,BTC-QRL,ETH-1ST,ETH-QRL,BTC-CRB,ETH-CRB,ETH-LGD,BTC-PTOY,ETH-PTOY,BTC-MYST,ETH-MYST,BTC-CFI,ETH-CFI,BTC-BNT,ETH-BNT,BTC-NMR,ETH-NMR,ETH-TIME,ETH-LTC,ETH-XRP,BTC-SNT,ETH-SNT,BTC-DCT,BTC-XEL,BTC-MCO,ETH-MCO,BTC-ADT,ETH-ADT,BTC-FUN,ETH-FUN,BTC-PAY,ETH-PAY,BTC-MTL,ETH-MTL,BTC-STORJ,ETH-STORJ,BTC-ADX,ETH-ADX,ETH-DASH,ETH-SC,ETH-ZEC,USDT-ZEC,USDT-LTC,USDT-ETC,USDT-XRP,BTC-OMG,ETH-OMG,BTC-CVC,ETH-CVC,BTC-PART,BTC-QTUM,ETH-QTUM,ETH-XMR,ETH-XEM,ETH-XLM,ETH-NEO,USDT-XMR,USDT-DASH,ETH-BCC,USDT-BCC,BTC-BCC,USDT-NEO,ETH-WAVES,ETH-STRAT,ETH-DGB,ETH-FCT,ETH-BTS", + "EnabledPairs": "USDT-BTC", + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "-" + }, + "RequestCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "-" + } }, { "Name": "BTCC", @@ -114,24 +147,16 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", "AvailablePairs": "BTCCNY,LTCCNY,LTCBTC", "EnabledPairs": "BTCCNY,LTCCNY,LTCBTC", - "BaseCurrencies": "CNY" - }, - { - "Name": "BTCE", - "Enabled": true, - "Verbose": false, - "Websocket": false, - "RESTPollingDelay": 10, - "AuthenticatedAPISupport": false, - "APIKey": "Key", - "APISecret": "Secret", - "ClientID": "", - "AvailablePairs": "BTCUSD,BTCRUR,BTCEUR,LTCBTC,LTCUSD,LTCRUR,LTCEUR,NMCBTC,NMCUSD,NVCBTC,NVCUSD,USDRUR,EURUSD,EURRUR,PPCBTC,PPCUSD", - "EnabledPairs": "BTCUSD,BTCRUR,BTCEUR,LTCBTC,LTCUSD,LTCRUR,LTCEUR,NMCBTC,NMCUSD,NVCBTC,NVCUSD,USDRUR,EURUSD,EURRUR,PPCBTC,PPCUSD", - "BaseCurrencies": "USD,RUR,EUR" + "BaseCurrencies": "CNY", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": false + } }, { "Name": "BTC Markets", @@ -142,10 +167,16 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", - "AvailablePairs": "LTC,BTC", - "EnabledPairs": "LTC,BTC", - "BaseCurrencies": "AUD" + "AvailablePairs": "LTCAUD,BTCAUD", + "EnabledPairs": "LTCAUD,BTCAUD", + "BaseCurrencies": "AUD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "COINUT", @@ -159,7 +190,14 @@ "ClientID": "ClientID", "AvailablePairs": "LTCBTC,ETCBTC,ETHBTC", "EnabledPairs": "LTCBTC,ETCBTC,ETHBTC", - "BaseCurrencies": "USD" + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "GDAX", @@ -171,9 +209,17 @@ "APIKey": "Key", "APISecret": "Secret", "ClientID": "ClientID", - "AvailablePairs": "BTCGBP,BTCEUR,ETHUSD,ETHBTC,LTCUSD,LTCBTC,BTCUSD", + "AvailablePairs": "LTCEUR,LTCBTC,BTCGBP,BTCEUR,ETHEUR,ETHBTC,LTCUSD,BTCUSD,ETHUSD", "EnabledPairs": "BTCUSD,BTCGBP,BTCEUR", - "BaseCurrencies": "USD,GBP,EUR" + "BaseCurrencies": "USD,GBP,EUR", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "-" + } }, { "Name": "Gemini", @@ -184,10 +230,16 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", "AvailablePairs": "BTCUSD,ETHBTC,ETHUSD", "EnabledPairs": "BTCUSD", - "BaseCurrencies": "USD" + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "Huobi", @@ -198,10 +250,16 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", "AvailablePairs": "BTCCNY,LTCCNY", "EnabledPairs": "BTCCNY,LTCCNY", - "BaseCurrencies": "CNY" + "BaseCurrencies": "CNY", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": false + } }, { "Name": "ITBIT", @@ -215,7 +273,14 @@ "ClientID": "ClientID", "AvailablePairs": "XBTUSD,XBTSGD,XBTEUR", "EnabledPairs": "XBTUSD,XBTSGD,XBTEUR", - "BaseCurrencies": "USD,SGD,EUR" + "BaseCurrencies": "USD,SGD,EUR", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "Kraken", @@ -226,10 +291,17 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", - "AvailablePairs": "ETCUSD,ICNETH,REPXBT,ZECXBT,ETHXBT,ETHXBT.d,ETHGBP,LTCXBT,XBTGBP.d,XDGXBT,XMRUSD,ZECUSD,ETCETH,ETHJPY,XBTCAD.d,XBTJPY.d,XBTUSD.d,XLMXBT,XLMEUR,XLMUSD,XMREUR,ETCXBT,ETHCAD.d,ETHEUR.d,ETHJPY.d,XBTEUR.d,ETHEUR,ETHGBP.d,ICNXBT,LTCEUR,REPEUR,XBTGBP,XBTJPY,ETHUSD,ETHUSD.d,LTCUSD,REPETH,XBTUSD,XMRXBT,ETCEUR,ETHCAD,REPUSD,XBTCAD,XBTEUR,XRPXBT,ZECEUR", + "AvailablePairs": "BCHEUR,REPEUR,XBTGBP,XBTUSD,ETHXBT,MLNXBT,ETCEUR,ETHGBP,ICNXBT,ZECEUR,EOSETH,GNOXBT,ETHCAD.D,ETHGBP.D,XRPEUR,BCHXBT,EOSXBT,LTCXBT,XBTEUR.D,XBTUSD.D,DASHUSD,GNOETH,ETHJPY,ETHUSD.D,REPETH,USDTUSD,ETHEUR,XLMXBT,BCHUSD,ETHCAD,XBTEUR,XMRUSD,ZECXBT,LTCUSD,XBTCAD,XMRXBT,ETHJPY.D,ICNETH,XBTCAD.D,XBTJPY,XRPUSD,ZECUSD,DASHEUR,ETCETH,ETCUSD,MLNETH,XMREUR,DASHXBT,ETHXBT.D,XDGXBT,XBTGBP.D,XRPXBT,XBTJPY.D,ETCXBT,ETHEUR.D,ETHUSD,LTCEUR,REPXBT", "EnabledPairs": "ETCUSD,XBTUSD,ETHUSD", - "BaseCurrencies": "EUR,USD,CAD,GBP,JPY" + "BaseCurrencies": "EUR,USD,CAD,GBP,JPY", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true, + "Separator": "," + } }, { "Name": "LakeBTC", @@ -240,10 +312,16 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", "AvailablePairs": "BTCUSD,BTCEUR,USDHKD,AUDUSD,BTCGBP,BTCNZD,USDJPY,BTCSGD,BTCNGN,EURUSD,USDSGD,NZDUSD,USDNGN,USDCHF,BTCJPY,BTCAUD,BTCCAD,BTCCHF,GBPUSD,USDCAD", "EnabledPairs": "BTCUSD,BTCAUD", - "BaseCurrencies": "USD,EUR,HKD,AUD,GBP,NZD,JPY,SGD,NGN,CHF,CAD" + "BaseCurrencies": "USD,EUR,HKD,AUD,GBP,NZD,JPY,SGD,NGN,CHF,CAD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "Liqui", @@ -254,10 +332,19 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", - "AvailablePairs": "TIME_BTC,ETH_BTC,GNT_BTC,WAVES_BTC,ICN_BTC,1ST_BTC,WINGS_BTC,MLN_BTC,ROUND_BTC,VSL_BTC,LTC_BTC,DCT_BTC,INCNT_BTC,PLU_BTC,DASH_BTC", + "AvailablePairs": "LUN_BTC,BCAP_ETH,NET_USDT,WAVES_ETH,GNO_ETH,CVC_ETH,GNO_BTC,XID_BTC,TAAS_BTC,MGO_ETH,STORJ_BTC,ADX_USDT,BCC_BTC,ICN_ETH,ETH_USDT,LUN_ETH,SNGLS_BTC,OMG_USDT,STX_BTC,RLC_USDT,TRST_BTC,STX_USDT,INCNT_ETH,EOS_BTC,CVC_USDT,NET_ETH,DGD_BTC,OAX_ETH,DNT_ETH,DASH_USDT,QTUM_BTC,TKN_USDT,SNM_USDT,MCO_ETH,SAN_ETH,TNT_ETH,ROUND_BTC,VSL_ETH,SAN_USDT,VSL_BTC,INCNT_BTC,STORJ_ETH,ZRX_ETH,BCAP_BTC,PTOY_ETH,PAY_BTC,MGO_USDT,EOS_USDT,TIME_USDT,INCNT_USDT,ANT_BTC,MYST_ETH,CFI_ETH,SNM_BTC,DASH_BTC,MLN_BTC,OMG_BTC,SAN_BTC,QTUM_ETH,LTC_ETH,QRL_ETH,QRL_USDT,BNT_ETH,QTUM_USDT,WAVES_USDT,REP_ETH,BNT_BTC,ETH_BTC,WINGS_USDT,SNGLS_ETH,XID_USDT,TNT_BTC,GNT_ETH,WINGS_ETH,BTC_USDT,GUP_USDT,TAAS_ETH,LUN_USDT,HMQ_ETH,MYST_BTC,WAVES_BTC,MLN_ETH,TNT_USDT,STORJ_USDT,OMG_ETH,EDG_BTC,GNO_USDT,BAT_ETH,SNT_USDT,DNT_BTC,PLU_ETH,REP_BTC,ADX_BTC,PAY_ETH,DGD_USDT,ZRX_BTC,WINGS_BTC,QRL_BTC,MCO_BTC,VSL_USDT,BAT_BTC,ANT_USDT,PAY_USDT,XID_ETH,TKN_BTC,EOS_ETH,NET_BTC,RLC_BTC,PTOY_BTC,SNM_ETH,OAX_BTC,1ST_ETH,BCAP_USDT,TRST_USDT,PLU_USDT,GUP_ETH,MCO_USDT,BCC_ETH,ROUND_ETH,TIME_ETH,TIME_BTC,ICN_USDT,GUP_BTC,SNGLS_USDT,PLU_BTC,MYST_USDT,CFI_USDT,SNT_BTC,SNT_ETH,ZRX_USDT,ICN_BTC,BAT_USDT,REP_USDT,HMQ_BTC,OAX_USDT,LTC_BTC,EDG_ETH,GNT_USDT,ROUND_USDT,BNT_USDT,CFI_BTC,CVC_BTC,BCC_USDT,GNT_BTC,STX_ETH,1ST_BTC,MGO_BTC,DNT_USDT,DASH_ETH,1ST_USDT,EDG_USDT,TKN_ETH,PTOY_USDT,ADX_ETH,LTC_USDT,RLC_ETH,HMQ_USDT,ANT_ETH,DGD_ETH,MLN_USDT,TRST_ETH,TAAS_USDT", "EnabledPairs": "ETH_BTC,LTC_BTC,DASH_BTC", - "BaseCurrencies": "USD" + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "_" + }, + "RequestCurrencyPairFormat": { + "Uppercase": false, + "Delimiter": "_", + "Separator": "-" + } }, { "Name": "LocalBitcoins", @@ -268,10 +355,16 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", "AvailablePairs": "BTCARS,BTCAUD,BTCBRL,BTCCAD,BTCCHF,BTCCZK,BTCDKK,BTCEUR,BTCGBP,BTCHKD,BTCILS,BTCINR,BTCMXN,BTCNOK,BTCNZD,BTCPLN,BTCRUB,BTCSEK,BTCSGD,BTCTHB,BTCUSD,BTCZAR", "EnabledPairs": "BTCARS,BTCAUD,BTCBRL,BTCCAD,BTCCHF,BTCCZK,BTCDKK,BTCEUR,BTCGBP,BTCHKD,BTCILS,BTCINR,BTCMXN,BTCNOK,BTCNZD,BTCPLN,BTCRUB,BTCSEK,BTCSGD,BTCTHB,BTCUSD,BTCZAR", - "BaseCurrencies": "ARS,AUD,BRL,CAD,CHF,CZK,DKK,EUR,GBP,HKD,ILS,INR,MXN,NOK,NZD,PLN,RUB,SEK,SGD,THB,USD,ZAR" + "BaseCurrencies": "ARS,AUD,BRL,CAD,CHF,CZK,DKK,EUR,GBP,HKD,ILS,INR,MXN,NOK,NZD,PLN,RUB,SEK,SGD,THB,USD,ZAR", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "OKCOIN China", @@ -282,10 +375,17 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", "AvailablePairs": "BTCCNY,LTCCNY", "EnabledPairs": "BTCCNY,LTCCNY", - "BaseCurrencies": "CNY" + "BaseCurrencies": "CNY", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": false, + "Delimiter": "_" + } }, { "Name": "OKCOIN International", @@ -296,10 +396,17 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", "AvailablePairs": "BTCUSD,LTCUSD", "EnabledPairs": "BTCUSD,LTCUSD", - "BaseCurrencies": "USD" + "BaseCurrencies": "USD", + "AssetTypes": "SPOT,this_week,next_week,quarter", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": false, + "Delimiter": "_" + } }, { "Name": "Poloniex", @@ -310,10 +417,40 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "ClientID": "", "AvailablePairs": "BTC_XUSD,BTC_FCT,BTC_MMNXT,BTC_NMC,BTC_BITUSD,BTC_RDD,BTC_XMR,BTC_XST,BTC_DSH,BTC_MAID,BTC_DGB,BTC_NEOS,BTC_BLK,BTC_NAUT,BTC_NBT,BTC_XCP,BTC_STR,BTC_BTCD,BTC_GRC,BTC_HUC,BTC_BBR,BTC_XDN,BTC_INDEX,BTC_IOC,BTC_SWARM,BTC_EMC2,BTC_MCN,BTC_NOXT,BTC_MINT,BTC_PTS,BTC_SC,BTC_GEO,BTC_XRP,BTC_FLO,BTC_BITS,BTC_HYP,BTC_XCR,BTC_LTBC,BTC_SYS,BTC_GMC,BTC_ETH,BTC_SYNC,BTC_GAP,BTC_BCN,BTC_C2,BTC_PINK,BTC_FIBRE,BTC_POT,BTC_QTL,BTC_SDC,BTC_XC,BTC_DASH,BTC_SILK,BTC_CLAM,BTC_NAV,BTC_PIGGY,BTC_BCY,BTC_MIL,BTC_XCN,BTC_YACC,BTC_BTS,BTC_QBK,BTC_SJCX,BTC_LQD,BTC_BURST,BTC_RIC,BTC_VRC,BTC_LTC,BTC_XPB,BTC_GRS,BTC_XCH,BTC_ARCH,BTC_QORA,BTC_HZ,BTC_NSR,BTC_XPM,BTC_BITCNY,BTC_EXE,BTC_XMG,BTC_BTC,BTC_BTM,BTC_NOBL,BTC_NXT,BTC_DOGE,BTC_CURE,BTC_MNTA,BTC_ADN,BTC_EXP,BTC_VTC,BTC_FLDC,BTC_MRS,BTC_MYR,BTC_OMNI,BTC_VNL,BTC_USDT,BTC_NOTE,BTC_WDC,BTC_BELA,BTC_VIA,BTC_CGA,BTC_DIEM,BTC_IFC,BTC_XDP,BTC_BLOCK,BTC_MMC,BTC_1CR,BTC_UNITY,BTC_XBC,BTC_GEMZ,BTC_FLT,BTC_PPC,BTC_XEM,BTC_RBY,BTC_CNMT,BTC_ABY,XMR_XDN,XMR_IFC,XMR_DIEM,XMR_BBR,XMR_DSH,XMR_BCN,XMR_LTC,XMR_MAID,XMR_DASH,XMR_BTCD,XMR_HYP,XMR_BLK,XMR_QORA,XMR_MNTA,XMR_NXT,USDT_BTC,USDT_ETH,USDT_XRP,USDT_DASH,USDT_LTC,USDT_NXT,USDT_XMR,USDT_STR", "EnabledPairs": "BTC_LTC,BTC_ETH,BTC_DOGE,BTC_DASH,BTC_XRP", - "BaseCurrencies": "USD" + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "_" + }, + "RequestCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "_" + } + }, + { + "Name": "WEX", + "Enabled": true, + "Verbose": false, + "Websocket": false, + "RESTPollingDelay": 10, + "AuthenticatedAPISupport": false, + "APIKey": "Key", + "APISecret": "Secret", + "AvailablePairs": "BTCUSD,BTCRUR,BTCEUR,LTCBTC,LTCUSD,LTCRUR,LTCEUR,NMCBTC,NMCUSD,NVCBTC,NVCUSD,USDRUR,EURUSD,EURRUR,PPCBTC,PPCUSD", + "EnabledPairs": "BTCUSD,BTCRUR,BTCEUR,LTCBTC,LTCUSD,LTCRUR,LTCEUR,NMCBTC,NMCUSD,NVCBTC,NVCUSD,USDRUR,EURUSD,EURRUR,PPCBTC,PPCUSD", + "BaseCurrencies": "USD,RUR,EUR", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": false, + "Delimiter": "_", + "Separator": "-" + } } ] -} +} \ No newline at end of file diff --git a/config_routes.go b/config_routes.go deleted file mode 100644 index 8de3474e..00000000 --- a/config_routes.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "encoding/json" - "net/http" - - "github.com/thrasher-/gocryptotrader/config" -) - -// GetAllSettings replies to a request with an encoded JSON response about the -// trading bots configuration. -func GetAllSettings(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(bot.config); err != nil { - panic(err) - } -} - -// SaveAllSettings saves all current settings from request body as a JSON -// document then reloads state and returns the settings -func SaveAllSettings(w http.ResponseWriter, r *http.Request) { - //Get the data from the request - decoder := json.NewDecoder(r.Body) - var responseData config.Post - jsonerr := decoder.Decode(&responseData) - if jsonerr != nil { - panic(jsonerr) - } - //Save change the settings - for x := range bot.config.Exchanges { - for i := 0; i < len(responseData.Data.Exchanges); i++ { - if responseData.Data.Exchanges[i].Name == bot.config.Exchanges[x].Name { - bot.config.Exchanges[x].Enabled = responseData.Data.Exchanges[i].Enabled - bot.config.Exchanges[x].APIKey = responseData.Data.Exchanges[i].APIKey - bot.config.Exchanges[x].APISecret = responseData.Data.Exchanges[i].APISecret - bot.config.Exchanges[x].EnabledPairs = responseData.Data.Exchanges[i].EnabledPairs - } - } - } - //Reload the configuration - err := bot.config.SaveConfig(bot.configFile) - if err != nil { - panic(err) - } - err = bot.config.LoadConfig(bot.configFile) - if err != nil { - panic(err) - } - setupBotExchanges() - //Return response status - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(bot.config); err != nil { - panic(err) - } -} - -// ConfigRoutes declares the current routes for config_routes.go -var ConfigRoutes = Routes{ - Route{ - "GetAllSettings", - "GET", - "/config/all", - GetAllSettings, - }, - - Route{ - "SaveAllSettings", - "POST", - "/config/all/save", - SaveAllSettings, - }, -} diff --git a/currency/currency.go b/currency/currency.go index 794cea42..0201f529 100644 --- a/currency/currency.go +++ b/currency/currency.go @@ -39,10 +39,18 @@ type YahooJSONResponse struct { } } +// FixerResponse contains the data fields for the Fixer API response +type FixerResponse struct { + Base string `json:"base"` + Date string `json:"date"` + Rates map[string]float64 `json:"rates"` +} + const ( maxCurrencyPairsPerRequest = 350 - yahooYQLURL = "http://query.yahooapis.com/v1/public/yql" + yahooYQLURL = "https://query.yahooapis.com/v1/public/yql?" yahooDatabase = "store://datatables.org/alltableswithkeys" + fixerAPI = "http://api.fixer.io/latest" // DefaultCurrencies has the default minimum of FIAT values DefaultCurrencies = "USD,AUD,EUR,CNY" // DefaultCryptoCurrencies has the default minimum of crytpocurrency values @@ -53,14 +61,45 @@ const ( // queries var ( CurrencyStore map[string]Rate + CurrencyStoreFixer map[string]float64 BaseCurrencies string CryptoCurrencies string ErrCurrencyDataNotFetched = errors.New("yahoo currency data has not been fetched yet") ErrCurrencyNotFound = errors.New("unable to find specified currency") ErrQueryingYahoo = errors.New("unable to query Yahoo currency values") ErrQueryingYahooZeroCount = errors.New("yahoo returned zero currency data") + YahooEnabled = true ) +// SetProvider sets the currency exchange service used by the currency +// converter +func SetProvider(yahooEnabled bool) { + if yahooEnabled { + YahooEnabled = true + return + } + YahooEnabled = false +} + +// SwapProvider swaps the currency exchange service used by the curency +// converter +func SwapProvider() { + if YahooEnabled { + YahooEnabled = false + return + } + YahooEnabled = true +} + +// GetProvider returns the currency exchange service used by the currency +// converter +func GetProvider() string { + if YahooEnabled { + return "yahoo" + } + return "fixer" +} + // IsDefaultCurrency checks if the currency passed in matches the default // FIAT currency func IsDefaultCurrency(currency string) bool { @@ -81,6 +120,7 @@ func IsDefaultCryptocurrency(currency string) bool { func IsFiatCurrency(currency string) bool { if BaseCurrencies == "" { log.Println("IsFiatCurrency: BaseCurrencies string variable not populated") + return false } return common.StringContains(BaseCurrencies, common.StringToUpper(currency)) } @@ -91,6 +131,7 @@ func IsCryptocurrency(currency string) bool { log.Println( "IsCryptocurrency: CryptoCurrencies string variable not populated", ) + return false } return common.StringContains(CryptoCurrencies, common.StringToUpper(currency)) } @@ -170,11 +211,11 @@ func SeedCurrencyData(fiatCurrencies string) error { fiatCurrencies = DefaultCurrencies } - err := QueryYahooCurrencyValues(fiatCurrencies) - if err != nil { - return ErrQueryingYahoo + if YahooEnabled { + return QueryYahooCurrencyValues(fiatCurrencies) } - return nil + + return FetchFixerCurrencyData() } // MakecurrencyPairs takes all supported currency and turns them into pairs. @@ -196,21 +237,90 @@ func MakecurrencyPairs(supportedCurrencies string) string { // ConvertCurrency for example converts $1 USD to the equivalent Japanese Yen // or vice versa. func ConvertCurrency(amount float64, from, to string) (float64, error) { - currency := common.StringToUpper(from + to) + from = common.StringToUpper(from) + to = common.StringToUpper(to) - _, ok := CurrencyStore[currency] + if from == to { + return amount, nil + } + + if YahooEnabled { + currency := from + to + _, ok := CurrencyStore[currency] + if !ok { + err := SeedCurrencyData(currency[:len(from)] + "," + currency[len(to):]) + if err != nil { + return 0, err + } + } + + result, ok := CurrencyStore[currency] + if !ok { + return 0, ErrCurrencyNotFound + } + return amount * result.Rate, nil + } + + _, ok := CurrencyStoreFixer[from] if !ok { - err := SeedCurrencyData(currency[:len(from)] + "," + currency[len(to):]) + err := FetchFixerCurrencyData() if err != nil { return 0, err } } - result, ok := CurrencyStore[currency] + var resultFrom float64 + var resultTo float64 + + // First check if we're converting to USD, USD doesn't exist in the rates map + if to == "USD" { + resultFrom, ok = CurrencyStoreFixer[from] + if !ok { + return 0, ErrCurrencyNotFound + } + return amount / resultFrom, nil + } + + // Check to see if we're converting from USD + if from == "USD" { + resultTo, ok = CurrencyStoreFixer[to] + if !ok { + return 0, ErrCurrencyNotFound + } + return resultTo * amount, nil + } + + // Otherwise convert to USD, then to the target currency + resultFrom, ok = CurrencyStoreFixer[from] if !ok { return 0, ErrCurrencyNotFound } - return amount * result.Rate, nil + + converted := amount / resultFrom + resultTo, ok = CurrencyStoreFixer[to] + if !ok { + return 0, ErrCurrencyNotFound + } + + return converted * resultTo, nil +} + +// FetchFixerCurrencyData seeds the variable C +func FetchFixerCurrencyData() error { + var result FixerResponse + values := url.Values{} + values.Set("base", "USD") + url := common.EncodeURLValues(fixerAPI, values) + + CurrencyStoreFixer = make(map[string]float64) + + err := common.SendHTTPGetRequest(url, true, false, &result) + if err != nil { + return err + } + + CurrencyStoreFixer = result.Rates + return nil } // FetchYahooCurrencyData seeds the variable CurrencyStore; this is a @@ -234,6 +344,8 @@ func FetchYahooCurrencyData(currencyPairs []string) error { return err } + log.Printf("Currency recv: %s", resp) + yahooResp := YahooJSONResponse{} err = common.JSONDecode([]byte(resp), &yahooResp) if err != nil { @@ -259,30 +371,5 @@ func QueryYahooCurrencyValues(currencies string) error { "%d fiat currency pairs generated. Fetching Yahoo currency data (this may take a minute)..\n", len(currencyPairs), ) - var err error - var pairs []string - index := 0 - - if len(currencyPairs) > maxCurrencyPairsPerRequest { - for index < len(currencyPairs) { - if len(currencyPairs)-index > maxCurrencyPairsPerRequest { - pairs = currencyPairs[index : index+maxCurrencyPairsPerRequest] - index += maxCurrencyPairsPerRequest - } else { - pairs = currencyPairs[index:] - index += (len(currencyPairs) - index) - } - err = FetchYahooCurrencyData(pairs) - if err != nil { - return err - } - } - } else { - pairs = currencyPairs[index:] - err = FetchYahooCurrencyData(pairs) - if err != nil { - return err - } - } - return nil + return FetchYahooCurrencyData(currencyPairs) } diff --git a/currency/currency_test.go b/currency/currency_test.go index 2fa00516..bee21e73 100644 --- a/currency/currency_test.go +++ b/currency/currency_test.go @@ -7,6 +7,65 @@ import ( "github.com/thrasher-/gocryptotrader/common" ) +func TestSetProvider(t *testing.T) { + defaultVal := YahooEnabled + expected := "yahoo" + SetProvider(true) + actual := GetProvider() + if expected != actual { + t.Errorf("Test failed. TestGetProvider expected %s got %s", expected, actual) + } + + SetProvider(false) + expected = "fixer" + actual = GetProvider() + if expected != actual { + t.Errorf("Test failed. TestGetProvider expected %s got %s", expected, actual) + } + + SetProvider(defaultVal) +} + +func TestSwapProvider(t *testing.T) { + defaultVal := YahooEnabled + expected := "fixer" + SetProvider(true) + SwapProvider() + actual := GetProvider() + if expected != actual { + t.Errorf("Test failed. TestGetProvider expected %s got %s", expected, actual) + } + + SetProvider(false) + SwapProvider() + expected = "yahoo" + actual = GetProvider() + if expected != actual { + t.Errorf("Test failed. TestGetProvider expected %s got %s", expected, actual) + } + + SetProvider(defaultVal) +} + +func TestGetProvider(t *testing.T) { + defaultVal := YahooEnabled + SetProvider(true) + expected := "yahoo" + actual := GetProvider() + if expected != actual { + t.Errorf("Test failed. TestGetProvider expected %s got %s", expected, actual) + } + + SetProvider(false) + expected = "fixer" + actual = GetProvider() + if expected != actual { + t.Errorf("Test failed. TestGetProvider expected %s got %s", expected, actual) + } + + SetProvider(defaultVal) +} + func TestIsDefaultCurrency(t *testing.T) { t.Parallel() @@ -58,6 +117,10 @@ func TestIsDefaultCryptocurrency(t *testing.T) { func TestIsFiatCurrency(t *testing.T) { t.Parallel() + if IsFiatCurrency("") { + t.Error("Test failed. TestIsFiatCurrency returned true on an empty string") + } + BaseCurrencies = "USD,AUD" var str1, str2, str3 string = "BTC", "USD", "birds123" @@ -81,6 +144,10 @@ func TestIsFiatCurrency(t *testing.T) { func TestIsCryptocurrency(t *testing.T) { t.Parallel() + if IsCryptocurrency("") { + t.Error("Test failed. TestIsCryptocurrency returned true on an empty string") + } + CryptoCurrencies = "BTC,LTC,DASH" var str1, str2, str3 string = "USD", "BTC", "pterodactyl123" @@ -256,6 +323,7 @@ func TestCheckAndAddCurrency(t *testing.T) { } func TestSeedCurrencyData(t *testing.T) { + SetProvider(true) currencyRequestDefault := "" currencyRequestUSDAUD := "USD,AUD" currencyRequestObtuse := "WigWham" @@ -281,6 +349,12 @@ func TestSeedCurrencyData(t *testing.T) { err3, currencyRequestObtuse, ) } + + SetProvider(false) + err = SeedCurrencyData("") + if err != nil { + t.Errorf("Test failed. SeedCurrencyData via Fixer. Error: %s", err) + } } func TestMakecurrencyPairs(t *testing.T) { @@ -297,30 +371,72 @@ func TestMakecurrencyPairs(t *testing.T) { } func TestConvertCurrency(t *testing.T) { + SetProvider(true) fiatCurrencies := DefaultCurrencies for _, currencyFrom := range common.SplitStrings(fiatCurrencies, ",") { for _, currencyTo := range common.SplitStrings(fiatCurrencies, ",") { - if currencyFrom == currencyTo { - continue - } else { - floatyMcfloat, err := ConvertCurrency(1000, currencyFrom, currencyTo) - if err != nil { - t.Errorf( - "Test Failed. ConvertCurrency: Error %s with return: %.2f Currency 1: %s Currency 2: %s", - err, floatyMcfloat, currencyFrom, currencyTo, - ) - } - if reflect.TypeOf(floatyMcfloat).String() != "float64" { - t.Error("Test Failed. ConvertCurrency: Error, incorrect return type") - } - if floatyMcfloat <= 0 { - t.Error( - "Test Failed. ConvertCurrency: Error, negative return or a serious issue with current fiat", - ) - } + floatyMcfloat, err := ConvertCurrency(1000, currencyFrom, currencyTo) + if err != nil { + t.Errorf( + "Test Failed. ConvertCurrency: Error %s with return: %.2f Currency 1: %s Currency 2: %s", + err, floatyMcfloat, currencyFrom, currencyTo, + ) + } + if reflect.TypeOf(floatyMcfloat).String() != "float64" { + t.Error("Test Failed. ConvertCurrency: Error, incorrect return type") + } + if floatyMcfloat <= 0 { + t.Error( + "Test Failed. ConvertCurrency: Error, negative return or a serious issue with current fiat", + ) } } } + + SetProvider(false) + _, err := ConvertCurrency(1000, "USD", "AUD") + if err != nil { + t.Errorf("Test failed. ConvertCurrency USD -> AUD. Error %s", err) + } + + _, err = ConvertCurrency(1000, "AUD", "USD") + if err != nil { + t.Errorf("Test failed. ConvertCurrency AUD -> AUD. Error %s", err) + } + + _, err = ConvertCurrency(1000, "CNY", "AUD") + if err != nil { + t.Errorf("Test failed. ConvertCurrency USD -> AUD. Error %s", err) + } + + // Test non-existant currencies + + _, err = ConvertCurrency(1000, "ASDF", "USD") + if err == nil { + t.Errorf("Test failed. ConvertCurrency non-existant currency -> USD. Error %s", err) + } + + _, err = ConvertCurrency(1000, "USD", "ASDF") + if err == nil { + t.Errorf("Test failed. ConvertCurrency USD -> non-existant currency. Error %s", err) + } + + _, err = ConvertCurrency(1000, "CNY", "UAHF") + if err == nil { + t.Errorf("Test failed. ConvertCurrency non-USD currency CNY -> non-existant currency. Error %s", err) + } + + _, err = ConvertCurrency(1000, "UASF", "UAHF") + if err == nil { + t.Errorf("Test failed. ConvertCurrency non-existant currency -> non-existant currency. Error %s", err) + } +} + +func TestFetchFixerCurrencyData(t *testing.T) { + err := FetchFixerCurrencyData() + if err != nil { + t.Errorf("Test failed. FetchFixerCurrencyData returned %s", err) + } } func TestFetchYahooCurrencyData(t *testing.T) { diff --git a/currency/pair/pair.go b/currency/pair/pair.go index 3e0c8b3e..a0ca789c 100644 --- a/currency/pair/pair.go +++ b/currency/pair/pair.go @@ -1,6 +1,8 @@ package pair -import "strings" +import ( + "strings" +) // CurrencyItem is an exported string with methods to manipulate the data instead // of using array/slice access modifiers @@ -43,7 +45,7 @@ func (c CurrencyPair) Pair() CurrencyItem { return c.FirstCurrency + CurrencyItem(c.Delimiter) + c.SecondCurrency } -// Display formats and returns the currency based on user preferences, +// Display formats and returns the currency based on user preferences, // overriding the default Pair() display func (c CurrencyPair) Display(delimiter string, uppercase bool) CurrencyItem { var pair CurrencyItem @@ -60,6 +62,15 @@ func (c CurrencyPair) Display(delimiter string, uppercase bool) CurrencyItem { return pair.Lower() } +// Equal compares two currency pairs and returns whether or not they are equal +func (c CurrencyPair) Equal(p CurrencyPair) bool { + if c.FirstCurrency.Upper() == p.FirstCurrency.Upper() && + c.SecondCurrency.Upper() == p.SecondCurrency.Upper() { + return true + } + return false +} + // NewCurrencyPairDelimiter splits the desired currency string at delimeter, // the returns a CurrencyPair struct func NewCurrencyPairDelimiter(currency, delimiter string) CurrencyPair { @@ -79,6 +90,16 @@ func NewCurrencyPair(firstCurrency, secondCurrency string) CurrencyPair { } } +// NewCurrencyPairFromIndex returns a CurrencyPair via a currency string and +// specific index +func NewCurrencyPairFromIndex(currency, index string) CurrencyPair { + i := strings.Index(currency, index) + if i == 0 { + return NewCurrencyPair(currency[0:len(index)], currency[len(index):]) + } + return NewCurrencyPair(currency[0:i], currency[i:]) +} + // NewCurrencyPairFromString converts currency string into a new CurrencyPair // with or without delimeter func NewCurrencyPairFromString(currency string) CurrencyPair { diff --git a/currency/pair/pair_test.go b/currency/pair/pair_test.go index 91abf22a..c8f6fcd0 100644 --- a/currency/pair/pair_test.go +++ b/currency/pair/pair_test.go @@ -105,6 +105,30 @@ func TestDisplay(t *testing.T) { } } +func TestEqual(t *testing.T) { + t.Parallel() + pair := NewCurrencyPair("BTC", "USD") + secondPair := NewCurrencyPair("btc", "uSd") + actual := pair.Equal(secondPair) + expected := true + if actual != expected { + t.Errorf( + "Test failed. Equal(): %v was not equal to expected value: %v", + actual, expected, + ) + } + + secondPair.SecondCurrency = "ETH" + actual = pair.Equal(secondPair) + expected = false + if actual != expected { + t.Errorf( + "Test failed. Equal(): %v was not equal to expected value: %v", + actual, expected, + ) + } +} + func TestNewCurrencyPair(t *testing.T) { t.Parallel() pair := NewCurrencyPair("BTC", "USD") @@ -140,6 +164,40 @@ func TestNewCurrencyPairDelimiter(t *testing.T) { } } +// NewCurrencyPairFromIndex returns a CurrencyPair via a currency string and +// specific index +func TestNewCurrencyPairFromIndex(t *testing.T) { + t.Parallel() + currency := "BTCUSD" + index := "BTC" + + pair := NewCurrencyPairFromIndex(currency, index) + pair.Delimiter = "-" + actual := pair.Pair() + + expected := CurrencyItem("BTC-USD") + if actual != expected { + t.Errorf( + "Test failed. Pair(): %s was not equal to expected value: %s", + actual, expected, + ) + } + + currency = "DOGEBTC" + + pair = NewCurrencyPairFromIndex(currency, index) + pair.Delimiter = "-" + actual = pair.Pair() + + expected = CurrencyItem("DOGE-BTC") + if actual != expected { + t.Errorf( + "Test failed. Pair(): %s was not equal to expected value: %s", + actual, expected, + ) + } +} + func TestNewCurrencyPairFromString(t *testing.T) { t.Parallel() pairStr := "BTC-USD" diff --git a/currency/symbol/symbol.go b/currency/symbol/symbol.go new file mode 100644 index 00000000..8f1cefb2 --- /dev/null +++ b/currency/symbol/symbol.go @@ -0,0 +1,125 @@ +package symbol + +import "errors" + +// symbols map holds the currency name and symbol mappings +var symbols = map[string]string{ + "ALL": "Lek", + "AFN": "؋", + "ARS": "$", + "AWG": "ƒ", + "AUD": "$", + "AZN": "ман", + "BSD": "$", + "BBD": "$", + "BYN": "Br", + "BZD": "BZ$", + "BMD": "$", + "BOB": "$b", + "BAM": "KM", + "BWP": "P", + "BGN": "лв", + "BRL": "R$", + "BND": "$", + "KHR": "៛", + "CAD": "$", + "KYD": "$", + "CLP": "$", + "CNY": "¥", + "COP": "$", + "CRC": "₡", + "HRK": "kn", + "CUP": "₱", + "CZK": "Kč", + "DKK": "kr", + "DOP": "RD$", + "XCD": "$", + "EGP": "£", + "SVC": "$", + "EUR": "€", + "FKP": "£", + "FJD": "$", + "GHS": "¢", + "GIP": "£", + "GTQ": "Q", + "GGP": "£", + "GYD": "$", + "HNL": "L", + "HKD": "$", + "HUF": "Ft", + "ISK": "kr", + "INR": "₹", + "IDR": "Rp", + "IRR": "﷼", + "IMP": "£", + "ILS": "₪", + "JMD": "J$", + "JPY": "¥", + "JEP": "£", + "KZT": "лв", + "KPW": "₩", + "KRW": "₩", + "KGS": "лв", + "LAK": "₭", + "LBP": "£", + "LRD": "$", + "MKD": "ден", + "MYR": "RM", + "MUR": "₨", + "MXN": "$", + "MNT": "₮", + "MZN": "MT", + "NAD": "$", + "NPR": "₨", + "ANG": "ƒ", + "NZD": "$", + "NIO": "C$", + "NGN": "₦", + "NOK": "kr", + "OMR": "﷼", + "PKR": "₨", + "PAB": "B/.", + "PYG": "Gs", + "PEN": "S/.", + "PHP": "₱", + "PLN": "zł", + "QAR": "﷼", + "RON": "lei", + "RUB": "₽", + "SHP": "£", + "SAR": "﷼", + "RSD": "Дин.", + "SCR": "₨", + "SGD": "$", + "SBD": "$", + "SOS": "S", + "ZAR": "R", + "LKR": "₨", + "SEK": "kr", + "CHF": "CHF", + "SRD": "$", + "SYP": "£", + "TWD": "NT$", + "THB": "฿", + "TTD": "TT$", + "TRY": "₺", + "TVD": "$", + "UAH": "₴", + "GBP": "£", + "USD": "$", + "UYU": "$U", + "UZS": "лв", + "VEF": "Bs", + "VND": "₫", + "YER": "﷼", + "ZWD": "Z$", +} + +// GetSymbolByCurrencyName returns a currency symbol +func GetSymbolByCurrencyName(currency string) (string, error) { + result, ok := symbols[currency] + if !ok { + return "", errors.New("currency symbol not found") + } + return result, nil +} diff --git a/currency/symbol/symbol_test.go b/currency/symbol/symbol_test.go new file mode 100644 index 00000000..5975cb67 --- /dev/null +++ b/currency/symbol/symbol_test.go @@ -0,0 +1,21 @@ +package symbol + +import "testing" + +func TestGetSymbolByCurrencyName(t *testing.T) { + expected := "₩" + actual, err := GetSymbolByCurrencyName("KPW") + if err != nil { + t.Errorf("Test failed. TestGetSymbolByCurrencyName error: %s", err) + } + + if actual != expected { + t.Errorf("Test failed. TestGetSymbolByCurrencyName differing values") + } + + _, err = GetSymbolByCurrencyName("BLAH") + if err == nil { + t.Errorf("Test failed. TestGetSymbolByCurrencyNam returned nil on non-existant currency") + } + +} diff --git a/events/event_test.go b/events/event_test.go index e9b9070b..93bf68c0 100644 --- a/events/event_test.go +++ b/events/event_test.go @@ -2,40 +2,64 @@ package events import ( "testing" + + "github.com/thrasher-/gocryptotrader/config" + "github.com/thrasher-/gocryptotrader/currency/pair" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" + "github.com/thrasher-/gocryptotrader/smsglobal" ) +var ( + loaded = false +) + +func testSetup(t *testing.T) { + if !loaded { + cfg := config.GetConfig() + err := cfg.LoadConfig("") + if err != nil { + t.Fatalf("Test failed. Failed to load config %s", err) + } + smsglobal.New(cfg.SMS.Username, cfg.SMS.Password, cfg.Name, cfg.SMS.Contacts) + loaded = true + } +} + func TestAddEvent(t *testing.T) { - eventID, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", actionTest) + testSetup(t) + + pair := pair.NewCurrencyPair("BTC", "USD") + eventID, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil && eventID != 0 { t.Errorf("Test Failed. AddEvent: Error, %s", err) } - eventID, err = AddEvent("ANXX", "price", ">,==", "BTC", "LTC", actionTest) + eventID, err = AddEvent("ANXX", "price", ">,==", pair, "SPOT", actionTest) if err == nil && eventID == 0 { t.Error("Test Failed. AddEvent: Error, error not captured in Exchange") } - eventID, err = AddEvent("ANX", "prices", ">,==", "BTC", "LTC", actionTest) + eventID, err = AddEvent("ANX", "prices", ">,==", pair, "SPOT", actionTest) if err == nil && eventID == 0 { t.Error("Test Failed. AddEvent: Error, error not captured in Item") } - eventID, err = AddEvent("ANX", "price", "3===D", "BTC", "LTC", actionTest) + eventID, err = AddEvent("ANX", "price", "3===D", pair, "SPOT", actionTest) if err == nil && eventID == 0 { t.Error("Test Failed. AddEvent: Error, error not captured in Condition") } - eventID, err = AddEvent("ANX", "price", ">,==", "BTC", "LTC", "console_prints") - if err == nil && eventID == 0 { - t.Error("Test Failed. AddEvent: Error, error not captured in Action") - } - eventID, err = AddEvent("ANX", "price", ">,==", "BATMAN", "ROBIN", actionTest) + eventID, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", "console_prints") if err == nil && eventID == 0 { t.Error("Test Failed. AddEvent: Error, error not captured in Action") } + if !RemoveEvent(eventID) { t.Error("Test Failed. RemoveEvent: Error, error removing event") } } func TestRemoveEvent(t *testing.T) { - eventID, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", actionTest) + testSetup(t) + + pair := pair.NewCurrencyPair("BTC", "USD") + eventID, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil && eventID != 0 { t.Errorf("Test Failed. RemoveEvent: Error, %s", err) } @@ -48,15 +72,18 @@ func TestRemoveEvent(t *testing.T) { } func TestGetEventCounter(t *testing.T) { - one, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", actionTest) + testSetup(t) + + pair := pair.NewCurrencyPair("BTC", "USD") + one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil { t.Errorf("Test Failed. GetEventCounter: Error, %s", err) } - two, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", actionTest) + two, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil { t.Errorf("Test Failed. GetEventCounter: Error, %s", err) } - three, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", actionTest) + three, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil { t.Errorf("Test Failed. GetEventCounter: Error, %s", err) } @@ -84,9 +111,12 @@ func TestGetEventCounter(t *testing.T) { } func TestExecuteAction(t *testing.T) { - one, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", actionTest) + testSetup(t) + + pair := pair.NewCurrencyPair("BTC", "USD") + one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil { - t.Errorf("Test Failed. ExecuteAction: Error, %s", err) + t.Fatalf("Test Failed. ExecuteAction: Error, %s", err) } isExecuted := Events[one].ExecuteAction() if !isExecuted { @@ -96,17 +126,47 @@ func TestExecuteAction(t *testing.T) { t.Error("Test Failed. ExecuteAction: Error, error removing event") } + action := actionSMSNotify + "," + "ALL" + one, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", action) + if err != nil { + t.Fatalf("Test Failed. ExecuteAction: Error, %s", err) + } + + isExecuted = Events[one].ExecuteAction() + if !isExecuted { + t.Error("Test Failed. ExecuteAction: Error, error removing event") + } + if !RemoveEvent(one) { + t.Error("Test Failed. ExecuteAction: Error, error removing event") + } + + action = actionSMSNotify + "," + "StyleGherkin" + one, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", action) + if err != nil { + t.Fatalf("Test Failed. ExecuteAction: Error, %s", err) + } + + isExecuted = Events[one].ExecuteAction() + if !isExecuted { + t.Error("Test Failed. ExecuteAction: Error, error removing event") + } + if !RemoveEvent(one) { + t.Error("Test Failed. ExecuteAction: Error, error removing event") + } // More tests when ExecuteAction is expanded } func TestEventToString(t *testing.T) { - one, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", actionTest) + testSetup(t) + + pair := pair.NewCurrencyPair("BTC", "USD") + one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil { t.Errorf("Test Failed. EventToString: Error, %s", err) } - eventString := Events[one].EventToString() - if eventString != "If the BTCLTC price on ANX is > == then ACTION_TEST." { + eventString := Events[one].String() + if eventString != "If the BTCUSD [SPOT] price on ANX is > == then ACTION_TEST." { t.Error("Test Failed. EventToString: Error, incorrect return string") } @@ -115,78 +175,142 @@ func TestEventToString(t *testing.T) { } } -func TestCheckCondition(t *testing.T) { //error handling needs to be implemented - one, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", actionTest) +func TestCheckCondition(t *testing.T) { + testSetup(t) + + // Test invalid currency pair + newPair := pair.NewCurrencyPair("A", "B") + one, err := AddEvent("ANX", "price", ">=,10", newPair, "SPOT", actionTest) if err != nil { - t.Errorf("Test Failed. EventToString: Error, %s", err) + t.Errorf("Test Failed. CheckCondition: Error, %s", err) + } + conditionBool := Events[one].CheckCondition() + if conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") } - conditionBool := Events[one].CheckCondition() - if conditionBool { //check once error handling is implemented - t.Error("Test Failed. EventToString: Error, wrong conditional.") + // Test last price == 0 + var tickerNew ticker.Price + tickerNew.Last = 0 + newPair = pair.NewCurrencyPair("BTC", "USD") + ticker.ProcessTicker("ANX", newPair, tickerNew, ticker.Spot) + Events[one].Pair = newPair + conditionBool = Events[one].CheckCondition() + if conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") + } + + // Test last pricce > 0 and conditional logic + tickerNew.Last = 11 + ticker.ProcessTicker("ANX", newPair, tickerNew, ticker.Spot) + Events[one].Condition = ">,10" + conditionBool = Events[one].CheckCondition() + if !conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") + } + + // Test last price >= 10 + Events[one].Condition = ">=,10" + conditionBool = Events[one].CheckCondition() + if !conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") + } + + // Test last price <= 10 + Events[one].Condition = "<,100" + conditionBool = Events[one].CheckCondition() + if !conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") + } + + // Test last price <= 10 + Events[one].Condition = "<=,100" + conditionBool = Events[one].CheckCondition() + if !conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") + } + + Events[one].Condition = "==,11" + conditionBool = Events[one].CheckCondition() + if !conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") + } + + Events[one].Condition = "^,11" + conditionBool = Events[one].CheckCondition() + if conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") } if !RemoveEvent(one) { - t.Error("Test Failed. EventToString: Error, error removing event") + t.Error("Test Failed. CheckCondition: Error, error removing event") } } func TestIsValidEvent(t *testing.T) { + testSetup(t) + err := IsValidEvent("ANX", "price", ">,==", actionTest) if err != nil { - t.Errorf("Test Failed. IsValidExchange: Error %s", err) + t.Errorf("Test Failed. IsValidEvent: %s", err) } err = IsValidEvent("ANX", "price", ">,", actionTest) if err == nil { - t.Errorf("Test Failed. IsValidExchange: Error") + t.Errorf("Test Failed. IsValidEvent: %s", err) } err = IsValidEvent("ANX", "Testy", ">,==", actionTest) if err == nil { - t.Errorf("Test Failed. IsValidExchange: Error") + t.Errorf("Test Failed. IsValidEvent: %s", err) } err = IsValidEvent("Testys", "price", ">,==", actionTest) if err == nil { - t.Errorf("Test Failed. IsValidExchange: Error") + t.Errorf("Test Failed. IsValidEvent: %s", err) + } + + action := "blah,blah" + err = IsValidEvent("ANX", "price", ">=,10", action) + if err == nil { + t.Errorf("Test Failed. IsValidEvent: %s", err) + } + + action = "SMS,blah" + err = IsValidEvent("ANX", "price", ">=,10", action) + if err == nil { + t.Errorf("Test Failed. IsValidEvent: %s", err) } //Function tests need to appended to this function when more actions are //implemented } -func TestCheckEvents(t *testing.T) { //Add error handling - //CheckEvents() //check once error handling is implemented -} +func TestCheckEvents(t *testing.T) { + testSetup(t) -func TestIsValidCurrency(t *testing.T) { - if !IsValidCurrency("BTC") { - t.Error("Test Failed - Event_test.go TestIsValidCurrency Error") - } - if !IsValidCurrency("USD") { - t.Error("Test Failed - Event_test.go TestIsValidCurrency Error") - } - if IsValidCurrency("testy") { - t.Error("Test Failed - Event_test.go TestIsValidCurrency Error") - } - if !IsValidCurrency("USD", "BTC", "USD") { - t.Error("Test Failed - Event_test.go TestIsValidCurrency Error") - } - if IsValidCurrency("USD", "USD", "Wigwham") { - t.Error("Test Failed - Event_test.go TestIsValidCurrency Error") + pair := pair.NewCurrencyPair("BTC", "USD") + _, err := AddEvent("ANX", "price", ">=,10", pair, "SPOT", actionTest) + if err != nil { + t.Fatal("Test failed. TestChcheckEvents add event") } + + go CheckEvents() } func TestIsValidExchange(t *testing.T) { - boolean := IsValidExchange("ANX", configPathTest) + testSetup(t) + + boolean := IsValidExchange("ANX") if !boolean { t.Error("Test Failed. IsValidExchange: Error, incorrect Exchange") } - boolean = IsValidExchange("OBTUSE", configPathTest) + boolean = IsValidExchange("OBTUSE") if boolean { t.Error("Test Failed. IsValidExchange: Error, incorrect return") } } func TestIsValidCondition(t *testing.T) { + testSetup(t) + boolean := IsValidCondition(">") if !boolean { t.Error("Test Failed. IsValidCondition: Error, incorrect Condition") @@ -214,6 +338,8 @@ func TestIsValidCondition(t *testing.T) { } func TestIsValidAction(t *testing.T) { + testSetup(t) + boolean := IsValidAction("sms") if !boolean { t.Error("Test Failed. IsValidAction: Error, incorrect Action") @@ -229,6 +355,8 @@ func TestIsValidAction(t *testing.T) { } func TestIsValidItem(t *testing.T) { + testSetup(t) + boolean := IsValidItem("price") if !boolean { t.Error("Test Failed. IsValidItem: Error, incorrect Item") diff --git a/events/events.go b/events/events.go index 5f3aa13e..e6b1a57f 100644 --- a/events/events.go +++ b/events/events.go @@ -8,7 +8,6 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" - "github.com/thrasher-/gocryptotrader/currency" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges/ticker" "github.com/thrasher-/gocryptotrader/smsglobal" @@ -24,7 +23,6 @@ const ( actionSMSNotify = "SMS" actionConsolePrint = "CONSOLE_PRINT" actionTest = "ACTION_TEST" - configPathTest = config.ConfigTestFile ) var ( @@ -32,19 +30,18 @@ var ( errInvalidCondition = errors.New("invalid conditional option") errInvalidAction = errors.New("invalid action") errExchangeDisabled = errors.New("desired exchange is disabled") - errCurrencyInvalid = errors.New("invalid currency") ) // Event struct holds the event variables type Event struct { - ID int - Exchange string - Item string - Condition string - FirstCurrency string - SecondCurrency string - Action string - Executed bool + ID int + Exchange string + Item string + Condition string + Pair pair.CurrencyPair + Asset string + Action string + Executed bool } // Events variable is a pointer array to the event structures that will be @@ -53,16 +50,12 @@ var Events []*Event // AddEvent adds an event to the Events chain and returns an index/eventID // and an error -func AddEvent(Exchange, Item, Condition, FirstCurrency, SecondCurrency, Action string) (int, error) { +func AddEvent(Exchange, Item, Condition string, CurrencyPair pair.CurrencyPair, Asset, Action string) (int, error) { err := IsValidEvent(Exchange, Item, Condition, Action) if err != nil { return 0, err } - if !IsValidCurrency(FirstCurrency, SecondCurrency) { - return 0, errCurrencyInvalid - } - Event := &Event{} if len(Events) == 0 { @@ -74,8 +67,8 @@ func AddEvent(Exchange, Item, Condition, FirstCurrency, SecondCurrency, Action s Event.Exchange = Exchange Event.Item = Item Event.Condition = Condition - Event.FirstCurrency = FirstCurrency - Event.SecondCurrency = SecondCurrency + Event.Pair = CurrencyPair + Event.Asset = Asset Event.Action = Action Event.Executed = false Events = append(Events, Event) @@ -112,27 +105,27 @@ func (e *Event) ExecuteAction() bool { if common.StringContains(e.Action, ",") { action := common.SplitStrings(e.Action, ",") if action[0] == actionSMSNotify { - message := fmt.Sprintf("Event triggered: %s", e.EventToString()) + message := fmt.Sprintf("Event triggered: %s", e.String()) + s := smsglobal.SMSGlobal if action[1] == "ALL" { - smsglobal.SMSSendToAll(message, config.Cfg) + s.SendMessageToAll(message) } else { - smsglobal.SMSNotify(smsglobal.SMSGetNumberByName(action[1], - config.Cfg.SMS), message, config.Cfg, - ) + contact, _ := s.GetContactByName(action[1]) + s.SendMessage(contact.Number, message) } } } else { - log.Printf("Event triggered: %s", e.EventToString()) + log.Printf("Event triggered: %s", e.String()) } return true } // EventToString turns the structure event into a string -func (e *Event) EventToString() string { +func (e *Event) String() string { condition := common.SplitStrings(e.Condition, ",") return fmt.Sprintf( - "If the %s%s %s on %s is %s then %s.", e.FirstCurrency, e.SecondCurrency, - e.Item, e.Exchange, condition[0]+" "+condition[1], e.Action, + "If the %s%s [%s] %s on %s is %s then %s.", e.Pair.FirstCurrency.String(), + e.Pair.SecondCurrency.String(), e.Asset, e.Item, e.Exchange, condition[0]+" "+condition[1], e.Action, ) } @@ -142,12 +135,12 @@ func (e *Event) CheckCondition() bool { condition := common.SplitStrings(e.Condition, ",") targetPrice, _ := strconv.ParseFloat(condition[1], 64) - ticker, err := ticker.GetTickerByExchange(e.Exchange) + t, err := ticker.GetTicker(e.Exchange, e.Pair, e.Asset) if err != nil { return false } - lastPrice := ticker.Price[pair.CurrencyItem(e.FirstCurrency)][pair.CurrencyItem(e.SecondCurrency)].Last + lastPrice := t.Last if lastPrice == 0 { return false @@ -194,12 +187,7 @@ func IsValidEvent(Exchange, Item, Condition, Action string) error { Item = common.StringToUpper(Item) Action = common.StringToUpper(Action) - configPath := "" - if Action == actionTest { - configPath = configPathTest - } - - if !IsValidExchange(Exchange, configPath) { + if !IsValidExchange(Exchange) { return errExchangeDisabled } @@ -224,9 +212,12 @@ func IsValidEvent(Exchange, Item, Condition, Action string) error { return errInvalidAction } - if action[1] != "ALL" && smsglobal.SMSGetNumberByName( - action[1], config.Cfg.SMS) == smsglobal.ErrSMSContactNotFound { - return errInvalidAction + if action[1] != "ALL" { + s := smsglobal.SMSGlobal + _, err := s.GetContactByName(action[1]) + if err != nil { + return errInvalidAction + } } } else { if Action != actionConsolePrint && Action != actionTest { @@ -258,29 +249,10 @@ func CheckEvents() { } } -// IsValidCurrency takes in CRYPTO or FIAT currency strings and returns if valid -func IsValidCurrency(currencies ...string) bool { - for index, whatIsIt := range currencies { - whatIsIt = common.StringToUpper(whatIsIt) - if currency.IsDefaultCryptocurrency(whatIsIt) || currency.IsDefaultCurrency(whatIsIt) { - if len(currencies)-1 == index { - return true - } - continue - } - } - return false -} - // IsValidExchange validates the exchange -func IsValidExchange(Exchange, configPath string) bool { +func IsValidExchange(Exchange string) bool { Exchange = common.StringToUpper(Exchange) - cfg := config.GetConfig() - if len(cfg.Exchanges) == 0 { - cfg.LoadConfig(configPath) - } - for _, x := range cfg.Exchanges { if x.Name == Exchange && x.Enabled { return true diff --git a/exchanges/alphapoint/alphapoint.go b/exchanges/alphapoint/alphapoint.go index 8da73698..327bc2dd 100644 --- a/exchanges/alphapoint/alphapoint.go +++ b/exchanges/alphapoint/alphapoint.go @@ -11,6 +11,7 @@ import ( "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( @@ -49,6 +50,7 @@ type Alphapoint struct { func (a *Alphapoint) SetDefaults() { a.APIUrl = alphapointDefaultAPIURL a.WebsocketURL = alphapointDefaultWebsocketURL + a.AssetTypes = []string{ticker.Spot} } // GetTicker returns current ticker information from Alphapoint for a selected diff --git a/exchanges/alphapoint/alphapoint_wrapper.go b/exchanges/alphapoint/alphapoint_wrapper.go index e9e1f34d..8ccbdfab 100644 --- a/exchanges/alphapoint/alphapoint_wrapper.go +++ b/exchanges/alphapoint/alphapoint_wrapper.go @@ -1,8 +1,6 @@ package alphapoint import ( - "log" - "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" @@ -30,28 +28,37 @@ func (a *Alphapoint) GetExchangeAccountInfo() (exchange.AccountInfo, error) { return response, nil } -// GetTickerPrice returns the current ticker price by currency pair -func (a *Alphapoint) GetTickerPrice(p pair.CurrencyPair) ticker.TickerPrice { - var tickerPrice ticker.TickerPrice +// UpdateTicker updates and returns the ticker for a currency pair +func (a *Alphapoint) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price tick, err := a.GetTicker(p.Pair().String()) if err != nil { - log.Println(err) - return ticker.TickerPrice{} + return tickerPrice, err } + tickerPrice.Pair = p tickerPrice.Ask = tick.Ask tickerPrice.Bid = tick.Bid - return tickerPrice + tickerPrice.Low = tick.Low + tickerPrice.High = tick.High + tickerPrice.Volume = tick.Volume + tickerPrice.Last = tick.Last + ticker.ProcessTicker(a.GetName(), p, tickerPrice, assetType) + return ticker.GetTicker(a.Name, p, assetType) } -// GetOrderbookEx returns an orderbookbase by currency pair -func (a *Alphapoint) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(a.GetName(), p) - if err == nil { - return ob, nil +// GetTickerPrice returns the ticker for a currency pair +func (a *Alphapoint) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tick, err := ticker.GetTicker(a.GetName(), p, assetType) + if err != nil { + return a.UpdateTicker(p, assetType) } + return tick, nil +} - var orderBook orderbook.OrderbookBase +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (a *Alphapoint) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base orderbookNew, err := a.GetOrderbook(p.Pair().String()) if err != nil { return orderBook, err @@ -59,15 +66,23 @@ func (a *Alphapoint) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBas for x := range orderbookNew.Bids { data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.OrderbookItem{Amount: data.Quantity, Price: data.Price}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data.Quantity, Price: data.Price}) } for x := range orderbookNew.Asks { data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.OrderbookItem{Amount: data.Quantity, Price: data.Price}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data.Quantity, Price: data.Price}) } - orderBook.Pair = p - orderbook.ProcessOrderbook(a.GetName(), p, orderBook) - return orderBook, nil + orderbook.ProcessOrderbook(a.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(a.Name, p, assetType) +} + +// GetOrderbookEx returns the orderbook for a currency pair +func (a *Alphapoint) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(a.GetName(), p, assetType) + if err == nil { + return a.UpdateOrderbook(p, assetType) + } + return ob, nil } diff --git a/exchanges/anx/anx.go b/exchanges/anx/anx.go index 92f25b89..b77b9240 100644 --- a/exchanges/anx/anx.go +++ b/exchanges/anx/anx.go @@ -11,6 +11,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( @@ -39,6 +40,13 @@ func (a *ANX) SetDefaults() { a.Verbose = false a.Websocket = false a.RESTPollingDelay = 10 + a.RequestCurrencyPairFormat.Delimiter = "" + a.RequestCurrencyPairFormat.Uppercase = true + a.RequestCurrencyPairFormat.Index = "BTC" + a.ConfigCurrencyPairFormat.Delimiter = "" + a.ConfigCurrencyPairFormat.Uppercase = true + a.ConfigCurrencyPairFormat.Index = "BTC" + a.AssetTypes = []string{ticker.Spot} } //Setup is run on startup to setup exchange with config values @@ -55,6 +63,14 @@ func (a *ANX) Setup(exch config.ExchangeConfig) { a.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") a.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") a.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := a.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = a.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } @@ -67,7 +83,7 @@ func (a *ANX) GetFee(maker bool) float64 { func (a *ANX) GetTicker(currency string) (ANXTicker, error) { var ticker ANXTicker - err := common.SendHTTPGetRequest(fmt.Sprintf("%sapi/2/%s/%s", ANX_API_URL, currency, ANX_TICKER), true, &ticker) + err := common.SendHTTPGetRequest(fmt.Sprintf("%sapi/2/%s/%s", ANX_API_URL, currency, ANX_TICKER), true, a.Verbose, &ticker) if err != nil { return ANXTicker{}, err } diff --git a/exchanges/anx/anx_test.go b/exchanges/anx/anx_test.go index 70ad4fd8..c5ede53e 100644 --- a/exchanges/anx/anx_test.go +++ b/exchanges/anx/anx_test.go @@ -35,6 +35,7 @@ func TestSetDefaults(t *testing.T) { func TestSetup(t *testing.T) { setup := ANX{} + setup.Name = "ANX" anxSetupConfig := config.GetConfig() anxSetupConfig.LoadConfig("../../testdata/configtest.dat") anxConfig, err := anxSetupConfig.GetExchangeConfig("ANX") @@ -49,7 +50,7 @@ func TestSetup(t *testing.T) { if setup.AuthenticatedAPISupport != false { t.Error("Test Failed - ANX Setup() incorrect values set") } - if len(setup.APIKey) <= 0 { + if len(setup.APIKey) != 0 { t.Error("Test Failed - ANX Setup() incorrect values set") } if len(setup.APISecret) != 0 { diff --git a/exchanges/anx/anx_types.go b/exchanges/anx/anx_types.go index 49b6956f..784fe4e9 100644 --- a/exchanges/anx/anx_types.go +++ b/exchanges/anx/anx_types.go @@ -30,11 +30,10 @@ type ANXOrderResponse struct { } type ANXTickerComponent struct { - Currency string `json:"currency"` - Display string `json:"display"` - DisplayShort string `json:"display_short"` - Value float64 `json:"value,string"` - ValueInt int64 `json:"value_int,string"` + Currency string `json:"currency"` + Display string `json:"display"` + DisplayShort string `json:"display_short"` + Value string `json:"value"` } type ANXTicker struct { diff --git a/exchanges/anx/anx_wrapper.go b/exchanges/anx/anx_wrapper.go index 6584a5ac..cf70d97b 100644 --- a/exchanges/anx/anx_wrapper.go +++ b/exchanges/anx/anx_wrapper.go @@ -2,72 +2,121 @@ package anx import ( "log" - "time" + "strconv" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +// Start starts the ANX go routine func (a *ANX) Start() { go a.Run() } +// Run implements the ANX wrapper func (a *ANX) Run() { if a.Verbose { log.Printf("%s polling delay: %ds.\n", a.GetName(), a.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", a.GetName(), len(a.EnabledPairs), a.EnabledPairs) } - - for a.Enabled { - for _, x := range a.EnabledPairs { - currency := pair.NewCurrencyPair(x[0:3], x[3:]) - go func() { - ticker, err := a.GetTickerPrice(currency) - if err != nil { - log.Println(err) - return - } - log.Printf("ANX %s: Last %f High %f Low %f Volume %f\n", exchange.FormatCurrency(currency).String(), ticker.Last, ticker.High, ticker.Low, ticker.Volume) - stats.AddExchangeInfo(a.GetName(), currency.GetFirstCurrency().String(), currency.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - }() - } - time.Sleep(time.Second * a.RESTPollingDelay) - } } -func (a *ANX) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - tickerNew, err := ticker.GetTicker(a.GetName(), p) - if err == nil { - return tickerNew, nil - } - - var tickerPrice ticker.TickerPrice - tick, err := a.GetTicker(p.Pair().String()) +// UpdateTicker updates and returns the ticker for a currency pair +func (a *ANX) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price + tick, err := a.GetTicker(exchange.FormatExchangeCurrency(a.GetName(), p).String()) if err != nil { return tickerPrice, err } tickerPrice.Pair = p - tickerPrice.Ask = tick.Data.Buy.Value - tickerPrice.Bid = tick.Data.Sell.Value - tickerPrice.Low = tick.Data.Low.Value - tickerPrice.Last = tick.Data.Last.Value - tickerPrice.Volume = tick.Data.Vol.Value - tickerPrice.High = tick.Data.High.Value - ticker.ProcessTicker(a.GetName(), p, tickerPrice) - return tickerPrice, nil + + if tick.Data.Sell.Value != "" { + tickerPrice.Ask, err = strconv.ParseFloat(tick.Data.Sell.Value, 64) + if err != nil { + return tickerPrice, err + } + } else { + tickerPrice.Ask = 0 + } + + if tick.Data.Buy.Value != "" { + tickerPrice.Bid, err = strconv.ParseFloat(tick.Data.Buy.Value, 64) + if err != nil { + return tickerPrice, err + } + } else { + tickerPrice.Bid = 0 + } + + if tick.Data.Low.Value != "" { + tickerPrice.Low, err = strconv.ParseFloat(tick.Data.Low.Value, 64) + if err != nil { + return tickerPrice, err + } + } else { + tickerPrice.Low = 0 + } + + if tick.Data.Last.Value != "" { + tickerPrice.Last, err = strconv.ParseFloat(tick.Data.Last.Value, 64) + if err != nil { + return tickerPrice, err + } + } else { + tickerPrice.Last = 0 + } + + if tick.Data.Vol.Value != "" { + tickerPrice.Volume, err = strconv.ParseFloat(tick.Data.Vol.Value, 64) + if err != nil { + return tickerPrice, err + } + } else { + tickerPrice.Volume = 0 + } + + if tick.Data.High.Value != "" { + tickerPrice.High, err = strconv.ParseFloat(tick.Data.High.Value, 64) + if err != nil { + return tickerPrice, err + } + } else { + tickerPrice.High = 0 + } + ticker.ProcessTicker(a.GetName(), p, tickerPrice, assetType) + return ticker.GetTicker(a.Name, p, assetType) } -func (e *ANX) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - return orderbook.OrderbookBase{}, nil +// GetTickerPrice returns the ticker for a currency pair +func (a *ANX) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(a.GetName(), p, assetType) + if err != nil { + return a.UpdateTicker(p, assetType) + } + return tickerNew, nil +} + +// GetOrderbookEx returns the orderbook for a currency pair +func (a *ANX) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(a.GetName(), p, assetType) + if err == nil { + return a.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (a *ANX) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + return orderBook, nil } //GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the ANX exchange -func (e *ANX) GetExchangeAccountInfo() (exchange.AccountInfo, error) { +func (a *ANX) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.ExchangeName = e.GetName() + response.ExchangeName = a.GetName() return response, nil } diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index f18e8bb5..b25ad80f 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -13,6 +13,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( @@ -81,6 +82,11 @@ func (b *Bitfinex) SetDefaults() { b.Websocket = false b.RESTPollingDelay = 10 b.WebsocketSubdChannels = make(map[int]WebsocketChanInfo) + b.RequestCurrencyPairFormat.Delimiter = "" + b.RequestCurrencyPairFormat.Uppercase = true + b.ConfigCurrencyPairFormat.Delimiter = "" + b.ConfigCurrencyPairFormat.Uppercase = true + b.AssetTypes = []string{ticker.Spot} } // Setup takes in the supplied exchange configuration details and sets params @@ -97,6 +103,14 @@ func (b *Bitfinex) Setup(exch config.ExchangeConfig) { b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := b.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = b.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } @@ -105,7 +119,7 @@ func (b *Bitfinex) GetTicker(symbol string, values url.Values) (Ticker, error) { response := Ticker{} path := common.EncodeURLValues(bitfinexAPIURL+bitfinexTicker+symbol, values) - return response, common.SendHTTPGetRequest(path, true, &response) + return response, common.SendHTTPGetRequest(path, true, b.Verbose, &response) } // GetStats returns various statistics about the requested pair @@ -113,7 +127,7 @@ func (b *Bitfinex) GetStats(symbol string) ([]Stat, error) { response := []Stat{} path := fmt.Sprint(bitfinexAPIURL + bitfinexStats + symbol) - return response, common.SendHTTPGetRequest(path, true, &response) + return response, common.SendHTTPGetRequest(path, true, b.Verbose, &response) } // GetFundingBook the entire margin funding book for both bids and asks sides @@ -123,7 +137,7 @@ func (b *Bitfinex) GetFundingBook(symbol string) (FundingBook, error) { response := FundingBook{} path := fmt.Sprint(bitfinexAPIURL + bitfinexLendbook + symbol) - return response, common.SendHTTPGetRequest(path, true, &response) + return response, common.SendHTTPGetRequest(path, true, b.Verbose, &response) } // GetOrderbook retieves the entire orderbook bid and ask price on a currency @@ -135,7 +149,7 @@ func (b *Bitfinex) GetOrderbook(currencyPair string, values url.Values) (Orderbo bitfinexAPIURL+bitfinexOrderbook+currencyPair, values, ) - return response, common.SendHTTPGetRequest(path, true, &response) + return response, common.SendHTTPGetRequest(path, true, b.Verbose, &response) } // GetTrades returns a list of the most recent trades for the given curencyPair @@ -146,7 +160,7 @@ func (b *Bitfinex) GetTrades(currencyPair string, values url.Values) ([]TradeStr bitfinexAPIURL+bitfinexTrades+currencyPair, values, ) - return response, common.SendHTTPGetRequest(path, true, &response) + return response, common.SendHTTPGetRequest(path, true, b.Verbose, &response) } // GetLendbook returns a list of the most recent funding data for the given @@ -160,7 +174,7 @@ func (b *Bitfinex) GetLendbook(symbol string, values url.Values) (Lendbook, erro } path := common.EncodeURLValues(bitfinexAPIURL+bitfinexLendbook+symbol, values) - return response, common.SendHTTPGetRequest(path, true, &response) + return response, common.SendHTTPGetRequest(path, true, b.Verbose, &response) } // GetLends returns a list of the most recent funding data for the given @@ -171,7 +185,7 @@ func (b *Bitfinex) GetLends(symbol string, values url.Values) ([]Lends, error) { response := []Lends{} path := common.EncodeURLValues(bitfinexAPIURL+bitfinexLends+symbol, values) - return response, common.SendHTTPGetRequest(path, true, &response) + return response, common.SendHTTPGetRequest(path, true, b.Verbose, &response) } // GetSymbols returns the avaliable currency pairs on the exchange @@ -179,7 +193,7 @@ func (b *Bitfinex) GetSymbols() ([]string, error) { products := []string{} path := fmt.Sprint(bitfinexAPIURL + bitfinexSymbols) - return products, common.SendHTTPGetRequest(path, true, &products) + return products, common.SendHTTPGetRequest(path, true, b.Verbose, &products) } // GetSymbolsDetails a list of valid symbol IDs and the pair details @@ -187,7 +201,7 @@ func (b *Bitfinex) GetSymbolsDetails() ([]SymbolDetails, error) { response := []SymbolDetails{} path := fmt.Sprint(bitfinexAPIURL + bitfinexSymbolsDetails) - return response, common.SendHTTPGetRequest(path, true, &response) + return response, common.SendHTTPGetRequest(path, true, b.Verbose, &response) } // GetAccountInfo returns information about your account incl. trading fees diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index 5bffc8ec..03363c5d 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -8,7 +8,6 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" - "github.com/thrasher-/gocryptotrader/currency" ) // Please supply your own keys here to do better tests @@ -30,24 +29,21 @@ func TestSetDefaults(t *testing.T) { } func TestSetup(t *testing.T) { - testConfig := config.ExchangeConfig{ - Enabled: true, - AuthenticatedAPISupport: true, - APIKey: testAPIKey, - APISecret: testAPISecret, - RESTPollingDelay: time.Duration(10), - Verbose: false, - Websocket: true, - BaseCurrencies: currency.DefaultCurrencies, - AvailablePairs: currency.MakecurrencyPairs(currency.DefaultCurrencies), - EnabledPairs: currency.MakecurrencyPairs(currency.DefaultCurrencies), + setup := Bitfinex{} + setup.Name = "Bitfinex" + cfg := config.GetConfig() + cfg.LoadConfig("../../testdata/configtest.dat") + bfxConfig, err := cfg.GetExchangeConfig("Bitfinex") + if err != nil { + t.Error("Test Failed - Bitfinex Setup() init error") } + setup.Setup(bfxConfig) - b.Setup(testConfig) + b.SetDefaults() + b.Setup(bfxConfig) - if !b.Enabled || !b.AuthenticatedAPISupport || b.APIKey != testAPIKey || - b.APISecret != testAPISecret || b.RESTPollingDelay != time.Duration(10) || - b.Verbose || !b.Websocket || len(b.BaseCurrencies) < 1 || + if !b.Enabled || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) || + b.Verbose || b.Websocket || len(b.BaseCurrencies) < 1 || len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 { t.Error("Test Failed - Bitfinex Setup values not set correctly") } diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 65730c16..a2033f31 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -2,22 +2,20 @@ package bitfinex import ( "log" - "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) -// Start starts a new wrapper through a go routine +// Start starts the Bitfinex go routine func (b *Bitfinex) Start() { go b.Run() } -// Run starts a new websocketclient connection and monitors ticker information +// Run implements the Bitfinex wrapper func (b *Bitfinex) Run() { if b.Verbose { log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket)) @@ -33,40 +31,21 @@ func (b *Bitfinex) Run() { if err != nil { log.Printf("%s Failed to get available symbols.\n", b.GetName()) } else { - err = b.UpdateAvailableCurrencies(exchangeProducts) + err = b.UpdateAvailableCurrencies(exchangeProducts, false) if err != nil { log.Printf("%s Failed to get config.\n", b.GetName()) } } - - for b.Enabled { - for _, x := range b.EnabledPairs { - currency := pair.NewCurrencyPair(x[0:3], x[3:]) - go func() { - ticker, err := b.GetTickerPrice(currency) - if err != nil { - return - } - log.Printf("Bitfinex %s Last %f High %f Low %f Volume %f\n", exchange.FormatCurrency(currency).String(), ticker.Last, ticker.High, ticker.Low, ticker.Volume) - stats.AddExchangeInfo(b.GetName(), currency.GetFirstCurrency().String(), currency.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - }() - } - time.Sleep(time.Second * b.RESTPollingDelay) - } } -// GetTickerPrice returns ticker information -func (b *Bitfinex) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - tick, err := ticker.GetTicker(b.GetName(), p) - if err == nil { - return tick, nil - } - - var tickerPrice ticker.TickerPrice +// UpdateTicker updates and returns the ticker for a currency pair +func (b *Bitfinex) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price tickerNew, err := b.GetTicker(p.Pair().String(), nil) if err != nil { return tickerPrice, err } + tickerPrice.Pair = p tickerPrice.Ask = tickerNew.Ask tickerPrice.Bid = tickerNew.Bid @@ -74,34 +53,46 @@ func (b *Bitfinex) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, erro tickerPrice.Last = tickerNew.Last tickerPrice.Volume = tickerNew.Volume tickerPrice.High = tickerNew.High - ticker.ProcessTicker(b.GetName(), p, tickerPrice) - return tickerPrice, nil + ticker.ProcessTicker(b.GetName(), p, tickerPrice, assetType) + return ticker.GetTicker(b.Name, p, assetType) } -// GetOrderbookEx returns orderbook information based on currency pair -func (b *Bitfinex) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(b.GetName(), p) - if err == nil { - return ob, nil +// GetTickerPrice returns the ticker for a currency pair +func (b *Bitfinex) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tick, err := ticker.GetTicker(b.GetName(), p, ticker.Spot) + if err != nil { + return b.UpdateTicker(p, assetType) } + return tick, nil +} - var orderBook orderbook.OrderbookBase +// GetOrderbookEx returns the orderbook for a currency pair +func (b *Bitfinex) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(b.GetName(), p, assetType) + if err == nil { + return b.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (b *Bitfinex) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base orderbookNew, err := b.GetOrderbook(p.Pair().String(), nil) if err != nil { return orderBook, err } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.OrderbookItem{Price: orderbookNew.Asks[x].Price, Amount: orderbookNew.Asks[x].Amount}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Price: orderbookNew.Asks[x].Price, Amount: orderbookNew.Asks[x].Amount}) } for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.OrderbookItem{Price: orderbookNew.Bids[x].Price, Amount: orderbookNew.Bids[x].Amount}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Price: orderbookNew.Bids[x].Price, Amount: orderbookNew.Bids[x].Amount}) } - orderBook.Pair = p - orderbook.ProcessOrderbook(b.GetName(), p, orderBook) - return orderBook, nil + orderbook.ProcessOrderbook(b.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(b.Name, p, assetType) } // GetExchangeAccountInfo retrieves balances for all enabled currencies on the diff --git a/exchanges/bitfinex/bitfinex_wrapper_test.go b/exchanges/bitfinex/bitfinex_wrapper_test.go index e1f634ad..a565c0ba 100644 --- a/exchanges/bitfinex/bitfinex_wrapper_test.go +++ b/exchanges/bitfinex/bitfinex_wrapper_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/thrasher-/gocryptotrader/currency/pair" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) func TestStart(t *testing.T) { @@ -18,7 +19,8 @@ func TestRun(t *testing.T) { func TestGetTickerPrice(t *testing.T) { getTickerPrice := Bitfinex{} - _, err := getTickerPrice.GetTickerPrice(pair.NewCurrencyPair("BTC", "USD")) + _, err := getTickerPrice.GetTickerPrice(pair.NewCurrencyPair("BTC", "USD"), + ticker.Spot) if err != nil { t.Errorf("Test Failed - Bitfinex GetTickerPrice() error: %s", err) } @@ -26,7 +28,8 @@ func TestGetTickerPrice(t *testing.T) { func TestGetOrderbookEx(t *testing.T) { getOrderBookEx := Bitfinex{} - _, err := getOrderBookEx.GetOrderbookEx(pair.NewCurrencyPair("BTC", "USD")) + _, err := getOrderBookEx.GetOrderbookEx(pair.NewCurrencyPair("BTC", "USD"), + ticker.Spot) if err != nil { t.Errorf("Test Failed - Bitfinex GetOrderbookEx() error: %s", err) } diff --git a/exchanges/bitstamp/bitstamp.go b/exchanges/bitstamp/bitstamp.go index b5ec92a1..fa385413 100644 --- a/exchanges/bitstamp/bitstamp.go +++ b/exchanges/bitstamp/bitstamp.go @@ -13,6 +13,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( @@ -60,6 +61,11 @@ func (b *Bitstamp) SetDefaults() { b.Verbose = false b.Websocket = false b.RESTPollingDelay = 10 + b.RequestCurrencyPairFormat.Delimiter = "" + b.RequestCurrencyPairFormat.Uppercase = true + b.ConfigCurrencyPairFormat.Delimiter = "" + b.ConfigCurrencyPairFormat.Uppercase = true + b.AssetTypes = []string{ticker.Spot} } // Setup sets configuration values to bitstamp @@ -76,6 +82,14 @@ func (b *Bitstamp) Setup(exch config.ExchangeConfig) { b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := b.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = b.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } @@ -113,7 +127,7 @@ func (b *Bitstamp) GetTicker(currency string, hourly bool) (Ticker, error) { tickerEndpoint, common.StringToLower(currency), ) - return response, common.SendHTTPGetRequest(path, true, &response) + return response, common.SendHTTPGetRequest(path, true, b.Verbose, &response) } // GetOrderbook Returns a JSON dictionary with "bids" and "asks". Each is a list @@ -135,7 +149,7 @@ func (b *Bitstamp) GetOrderbook(currency string) (Orderbook, error) { common.StringToLower(currency), ) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, b.Verbose, &resp) if err != nil { return Orderbook{}, err } @@ -190,7 +204,7 @@ func (b *Bitstamp) GetTransactions(currencyPair string, values url.Values) ([]Tr values, ) - return transactions, common.SendHTTPGetRequest(path, true, &transactions) + return transactions, common.SendHTTPGetRequest(path, true, b.Verbose, &transactions) } // GetEURUSDConversionRate returns the conversion rate between Euro and USD @@ -198,7 +212,7 @@ func (b *Bitstamp) GetEURUSDConversionRate() (EURUSDConversionRate, error) { rate := EURUSDConversionRate{} path := fmt.Sprintf("%s/%s", bitstampAPIURL, bitstampAPIEURUSD) - return rate, common.SendHTTPGetRequest(path, true, &rate) + return rate, common.SendHTTPGetRequest(path, true, b.Verbose, &rate) } // GetBalance returns full balance of currency held on the exchange diff --git a/exchanges/bitstamp/bitstamp_test.go b/exchanges/bitstamp/bitstamp_test.go index e7133de2..ce298211 100644 --- a/exchanges/bitstamp/bitstamp_test.go +++ b/exchanges/bitstamp/bitstamp_test.go @@ -3,6 +3,7 @@ package bitstamp import ( "net/url" "testing" + "time" "github.com/thrasher-/gocryptotrader/config" ) @@ -39,18 +40,29 @@ func TestSetDefaults(t *testing.T) { func TestSetup(t *testing.T) { t.Parallel() b := Bitstamp{} - conf := config.ExchangeConfig{ - Name: "bla", - Enabled: true, - AuthenticatedAPISupport: true, + b.Name = "Bitstamp" + cfg := config.GetConfig() + cfg.LoadConfig("../../testdata/configtest.dat") + bConfig, err := cfg.GetExchangeConfig("Bitstamp") + if err != nil { + t.Error("Test Failed - Bitstamp Setup() init error") } - b.Setup(conf) - if b.Name != "bla" && b.Enabled != true && b.AuthenticatedAPISupport != true { - t.Error("Test Failed - Setup() error") + b.SetDefaults() + b.Setup(bConfig) + + if !b.IsEnabled() || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) || + b.Verbose || b.Websocket || len(b.BaseCurrencies) < 1 || + len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 { + t.Error("Test Failed - Bitstamp Setup values not set correctly") + } + + bConfig.Enabled = false + b.Setup(bConfig) + + if b.IsEnabled() { + t.Error("Test failed - Bitstamp TestSetup incorrect value") } - conf.Enabled = false - b.Setup(conf) } func TestGetFee(t *testing.T) { diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index c4988c78..b8183e99 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -2,22 +2,20 @@ package bitstamp import ( "log" - "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency/pair" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) -// Start starts a new go routine run +// Start starts the Bitstamp go routine func (b *Bitstamp) Start() { go b.Run() } -// Run starts a new websocket connection runs a new go routine pusher +// Run implements the Bitstamp wrapper func (b *Bitstamp) Run() { if b.Verbose { log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket)) @@ -28,32 +26,11 @@ func (b *Bitstamp) Run() { if b.Websocket { go b.PusherClient() } - - for b.Enabled { - for _, x := range b.EnabledPairs { - currency := pair.NewCurrencyPair(x[0:3], x[3:]) - go func() { - ticker, err := b.GetTickerPrice(currency) - if err != nil { - log.Println(err) - return - } - log.Printf("Bitstamp %s: Last %f High %f Low %f Volume %f\n", exchange.FormatCurrency(currency).String(), ticker.Last, ticker.High, ticker.Low, ticker.Volume) - stats.AddExchangeInfo(b.GetName(), currency.GetFirstCurrency().String(), currency.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - }() - } - time.Sleep(time.Second * b.RESTPollingDelay) - } } -// GetTickerPrice returns ticker price information -func (b *Bitstamp) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - tickerNew, err := ticker.GetTicker(b.GetName(), p) - if err == nil { - return tickerNew, nil - } - - var tickerPrice ticker.TickerPrice +// UpdateTicker updates and returns the ticker for a currency pair +func (b *Bitstamp) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price tick, err := b.GetTicker(p.Pair().String(), false) if err != nil { return tickerPrice, err @@ -66,18 +43,31 @@ func (b *Bitstamp) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, erro tickerPrice.Last = tick.Last tickerPrice.Volume = tick.Volume tickerPrice.High = tick.High - ticker.ProcessTicker(b.GetName(), p, tickerPrice) - return tickerPrice, nil + ticker.ProcessTicker(b.GetName(), p, tickerPrice, assetType) + return ticker.GetTicker(b.Name, p, assetType) } -// GetOrderbookEx returns base orderbook information -func (b *Bitstamp) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(b.GetName(), p) - if err == nil { - return ob, nil +// GetTickerPrice returns the ticker for a currency pair +func (b *Bitstamp) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tick, err := ticker.GetTicker(b.GetName(), p, assetType) + if err != nil { + return b.UpdateTicker(p, assetType) } + return tick, nil +} - var orderBook orderbook.OrderbookBase +// GetOrderbookEx returns the orderbook for a currency pair +func (b *Bitstamp) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(b.GetName(), p, assetType) + if err == nil { + return b.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (b *Bitstamp) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base orderbookNew, err := b.GetOrderbook(p.Pair().String()) if err != nil { return orderBook, err @@ -85,17 +75,16 @@ func (b *Bitstamp) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, for x := range orderbookNew.Bids { data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.OrderbookItem{Amount: data.Amount, Price: data.Price}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data.Amount, Price: data.Price}) } for x := range orderbookNew.Asks { data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.OrderbookItem{Amount: data.Amount, Price: data.Price}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data.Amount, Price: data.Price}) } - orderBook.Pair = p - orderbook.ProcessOrderbook(b.GetName(), p, orderBook) - return orderBook, nil + orderbook.ProcessOrderbook(b.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(b.Name, p, assetType) } // GetExchangeAccountInfo retrieves balances for all enabled currencies for the diff --git a/exchanges/bittrex/bittrex.go b/exchanges/bittrex/bittrex.go index cdcb857a..13c878c5 100644 --- a/exchanges/bittrex/bittrex.go +++ b/exchanges/bittrex/bittrex.go @@ -13,6 +13,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( @@ -66,6 +67,11 @@ func (b *Bittrex) SetDefaults() { b.Verbose = false b.Websocket = false b.RESTPollingDelay = 10 + b.RequestCurrencyPairFormat.Delimiter = "-" + b.RequestCurrencyPairFormat.Uppercase = true + b.ConfigCurrencyPairFormat.Delimiter = "-" + b.ConfigCurrencyPairFormat.Uppercase = true + b.AssetTypes = []string{ticker.Spot} } // Setup method sets current configuration details if enabled @@ -82,6 +88,14 @@ func (b *Bittrex) Setup(exch config.ExchangeConfig) { b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := b.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = b.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } @@ -359,7 +373,7 @@ func (b *Bittrex) HTTPRequest(path string, auth bool, values url.Values, v inter return err } } else { - if err := common.SendHTTPGetRequest(path, true, &response); err != nil { + if err := common.SendHTTPGetRequest(path, true, b.Verbose, &response); err != nil { return err } } diff --git a/exchanges/bittrex/bittrex_test.go b/exchanges/bittrex/bittrex_test.go index 32f7786f..99e5e209 100644 --- a/exchanges/bittrex/bittrex_test.go +++ b/exchanges/bittrex/bittrex_test.go @@ -2,6 +2,7 @@ package bittrex import ( "testing" + "time" "github.com/thrasher-/gocryptotrader/config" ) @@ -23,20 +24,29 @@ func TestSetDefaults(t *testing.T) { func TestSetup(t *testing.T) { t.Parallel() - exch := config.ExchangeConfig{ - Name: "Bittrex", - APIKey: apiKey, - } - exch.Enabled = true b := Bittrex{} - b.Setup(exch) - if b.APIKey != apiKey { - t.Error("Test Failed - Bittrex - Setup() error") + b.Name = "Bittrex" + cfg := config.GetConfig() + cfg.LoadConfig("../../testdata/configtest.dat") + bConfig, err := cfg.GetExchangeConfig("Bittrex") + if err != nil { + t.Error("Test Failed - Bittrex Setup() init error") } - exch.Enabled = false - b.Setup(exch) + + b.SetDefaults() + b.Setup(bConfig) + + if !b.IsEnabled() || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) || + b.Verbose || b.Websocket || len(b.BaseCurrencies) < 1 || + len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 { + t.Error("Test Failed - Bittrex Setup values not set correctly") + } + + bConfig.Enabled = false + b.Setup(bConfig) + if b.IsEnabled() { - t.Error("Test Failed - Bittrex - Setup() error") + t.Error("Test failed - Bittrex TestSetup incorrect value") } } diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index 7689a478..9aff0a89 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -2,17 +2,15 @@ package bittrex import ( "log" - "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) -// Start stats the Bittrex go routine +// Start starts the Bittrex go routine func (b *Bittrex) Start() { go b.Run() } @@ -28,39 +26,36 @@ func (b *Bittrex) Run() { if err != nil { log.Printf("%s Failed to get available symbols.\n", b.GetName()) } else { + forceUpgrade := false + if !common.DataContains(b.EnabledPairs, "-") || !common.DataContains(b.AvailablePairs, "-") { + forceUpgrade = true + } var currencies []string for x := range exchangeProducts { - if !exchangeProducts[x].IsActive { + if !exchangeProducts[x].IsActive || exchangeProducts[x].MarketName == "" { continue } - currencies = append(currencies, - common.ReplaceString(exchangeProducts[x].MarketName, "-", "", -1)) + currencies = append(currencies, exchangeProducts[x].MarketName) } - err = b.UpdateAvailableCurrencies(currencies) + + if forceUpgrade { + enabledPairs := []string{"USDT-BTC"} + log.Println("WARNING: Available pairs for Bittrex reset due to config upgrade, please enable the ones you would like again") + + err = b.UpdateEnabledCurrencies(enabledPairs, true) + if err != nil { + log.Printf("%s Failed to get config.\n", b.GetName()) + } + } + err = b.UpdateAvailableCurrencies(currencies, forceUpgrade) if err != nil { log.Printf("%s Failed to get config.\n", b.GetName()) } } - - for b.Enabled { - for _, x := range b.EnabledPairs { - currency := pair.NewCurrencyPair(x[0:3], x[3:]) - currency.Delimiter = "-" - go func() { - ticker, err := b.GetTickerPrice(currency) - if err != nil { - log.Println(err) - return - } - log.Printf("Bittrex %s Last %f Bid %f Ask %f Volume %f\n", exchange.FormatCurrency(currency).String(), ticker.Last, ticker.Bid, ticker.Ask, ticker.Volume) - stats.AddExchangeInfo(b.GetName(), currency.GetFirstCurrency().String(), currency.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - }() - } - time.Sleep(time.Second * b.RESTPollingDelay) - } } -//GetExchangeAccountInfo Retrieves balances for all enabled currencies for the Bittrexexchange +// GetExchangeAccountInfo Retrieves balances for all enabled currencies for the +// Bittrex exchange func (b *Bittrex) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo response.ExchangeName = b.GetName() @@ -79,15 +74,10 @@ func (b *Bittrex) GetExchangeAccountInfo() (exchange.AccountInfo, error) { return response, nil } -// GetTickerPrice returns the ticker for a currencyp pair -func (b *Bittrex) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - tickerNew, err := ticker.GetTicker(b.GetName(), p) - if err == nil { - return tickerNew, nil - } - - var tickerPrice ticker.TickerPrice - tick, err := b.GetMarketSummary(p.Pair().Lower().String()) +// UpdateTicker updates and returns the ticker for a currency pair +func (b *Bittrex) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price + tick, err := b.GetMarketSummary(exchange.FormatExchangeCurrency(b.GetName(), p).String()) if err != nil { return tickerPrice, err } @@ -96,26 +86,39 @@ func (b *Bittrex) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error tickerPrice.Bid = tick[0].Bid tickerPrice.Last = tick[0].Last tickerPrice.Volume = tick[0].Volume - ticker.ProcessTicker(b.GetName(), p, tickerPrice) - return tickerPrice, nil + ticker.ProcessTicker(b.GetName(), p, tickerPrice, assetType) + return ticker.GetTicker(b.Name, p, assetType) } -// GetOrderbookEx returns the orderbook for a currencyp pair -func (b *Bittrex) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(b.GetName(), p) - if err == nil { - return ob, nil +// GetTickerPrice returns the ticker for a currency pair +func (b *Bittrex) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tick, err := ticker.GetTicker(b.GetName(), p, ticker.Spot) + if err != nil { + return b.UpdateTicker(p, assetType) } + return tick, nil +} - var orderBook orderbook.OrderbookBase - orderbookNew, err := b.GetOrderbook(p.Pair().Lower().String()) +// GetOrderbookEx returns the orderbook for a currency pair +func (b *Bittrex) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(b.GetName(), p, assetType) + if err == nil { + return b.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (b *Bittrex) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + orderbookNew, err := b.GetOrderbook(exchange.FormatExchangeCurrency(b.GetName(), p).String()) if err != nil { return orderBook, err } for x := range orderbookNew.Buy { orderBook.Bids = append(orderBook.Bids, - orderbook.OrderbookItem{ + orderbook.Item{ Amount: orderbookNew.Buy[x].Quantity, Price: orderbookNew.Buy[x].Rate, }, @@ -124,14 +127,13 @@ func (b *Bittrex) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, for x := range orderbookNew.Sell { orderBook.Asks = append(orderBook.Asks, - orderbook.OrderbookItem{ + orderbook.Item{ Amount: orderbookNew.Sell[x].Quantity, Price: orderbookNew.Sell[x].Rate, }, ) } - orderBook.Pair = p - orderbook.ProcessOrderbook(b.GetName(), p, orderBook) - return orderBook, nil + orderbook.ProcessOrderbook(b.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(b.Name, p, assetType) } diff --git a/exchanges/btcc/btcc.go b/exchanges/btcc/btcc.go index eb4beffa..dd5b12d4 100644 --- a/exchanges/btcc/btcc.go +++ b/exchanges/btcc/btcc.go @@ -12,6 +12,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( @@ -55,6 +56,11 @@ func (b *BTCC) SetDefaults() { b.Verbose = false b.Websocket = false b.RESTPollingDelay = 10 + b.RequestCurrencyPairFormat.Delimiter = "" + b.RequestCurrencyPairFormat.Uppercase = false + b.ConfigCurrencyPairFormat.Delimiter = "" + b.ConfigCurrencyPairFormat.Uppercase = true + b.AssetTypes = []string{ticker.Spot} } // Setup is run on startup to setup exchange with config values @@ -71,6 +77,14 @@ func (b *BTCC) Setup(exch config.ExchangeConfig) { b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := b.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = b.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } @@ -85,7 +99,7 @@ func (b *BTCC) GetTicker(currencyPair string) (Ticker, error) { resp := Response{} req := fmt.Sprintf("%sdata/ticker?market=%s", btccAPIUrl, currencyPair) - return resp.Ticker, common.SendHTTPGetRequest(req, true, &resp) + return resp.Ticker, common.SendHTTPGetRequest(req, true, b.Verbose, &resp) } // GetTradesLast24h returns the trades executed on the exchange over the past @@ -95,7 +109,7 @@ func (b *BTCC) GetTradesLast24h(currencyPair string) ([]Trade, error) { trades := []Trade{} req := fmt.Sprintf("%sdata/trades?market=%s", btccAPIUrl, currencyPair) - return trades, common.SendHTTPGetRequest(req, true, &trades) + return trades, common.SendHTTPGetRequest(req, true, b.Verbose, &trades) } // GetTradeHistory returns trade history data @@ -122,7 +136,7 @@ func (b *BTCC) GetTradeHistory(currencyPair string, limit, sinceTid int64, time req = common.EncodeURLValues(req, v) - return trades, common.SendHTTPGetRequest(req, true, &trades) + return trades, common.SendHTTPGetRequest(req, true, b.Verbose, &trades) } // GetOrderBook returns current market order book @@ -137,7 +151,7 @@ func (b *BTCC) GetOrderBook(currencyPair string, limit int) (Orderbook, error) { req = fmt.Sprintf("%sdata/orderbook?market=%s", btccAPIUrl, currencyPair) } - return result, common.SendHTTPGetRequest(req, true, &result) + return result, common.SendHTTPGetRequest(req, true, b.Verbose, &result) } func (b *BTCC) GetAccountInfo(infoType string) error { diff --git a/exchanges/btcc/btcc_test.go b/exchanges/btcc/btcc_test.go index aca0c326..6108fcc8 100644 --- a/exchanges/btcc/btcc_test.go +++ b/exchanges/btcc/btcc_test.go @@ -20,17 +20,31 @@ func TestSetDefaults(t *testing.T) { } func TestSetup(t *testing.T) { - conf := config.ExchangeConfig{ - Enabled: true, + t.Parallel() + b := BTCC{} + b.Name = "BTCC" + cfg := config.GetConfig() + cfg.LoadConfig("../../testdata/configtest.dat") + bConfig, err := cfg.GetExchangeConfig("BTCC") + if err != nil { + t.Error("Test Failed - BTCC Setup() init error") } - b.Setup(conf) - conf = config.ExchangeConfig{ - Enabled: false, - APIKey: apiKey, - APISecret: apiSecret, + b.SetDefaults() + b.Setup(bConfig) + + if !b.IsEnabled() || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) || + b.Verbose || b.Websocket || len(b.BaseCurrencies) < 1 || + len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 { + t.Error("Test Failed - BTCC Setup values not set correctly") + } + + bConfig.Enabled = false + b.Setup(bConfig) + + if b.IsEnabled() { + t.Error("Test failed - BTCC TestSetup incorrect value") } - b.Setup(conf) } func TestGetFee(t *testing.T) { diff --git a/exchanges/btcc/btcc_wrapper.go b/exchanges/btcc/btcc_wrapper.go index 88fca71c..a3dcf97b 100644 --- a/exchanges/btcc/btcc_wrapper.go +++ b/exchanges/btcc/btcc_wrapper.go @@ -2,20 +2,20 @@ package btcc import ( "log" - "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency/pair" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +// Start starts the BTCC go routine func (b *BTCC) Start() { go b.Run() } +// Run implements the BTCC wrapper func (b *BTCC) Run() { if b.Verbose { log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket)) @@ -26,36 +26,15 @@ func (b *BTCC) Run() { if b.Websocket { go b.WebsocketClient() } - - for b.Enabled { - for _, x := range b.EnabledPairs { - currency := pair.NewCurrencyPair(x[0:3], x[3:]) - go func() { - ticker, err := b.GetTickerPrice(currency) - if err != nil { - log.Println(err) - return - } - log.Printf("BTCC %s: Last %f High %f Low %f Volume %f\n", exchange.FormatCurrency(currency).String(), ticker.Last, ticker.High, ticker.Low, ticker.Volume) - stats.AddExchangeInfo(b.GetName(), currency.GetFirstCurrency().String(), currency.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - }() - } - time.Sleep(time.Second * b.RESTPollingDelay) - } } -func (b *BTCC) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - tickerNew, err := ticker.GetTicker(b.GetName(), p) - if err == nil { - return tickerNew, nil - } - - var tickerPrice ticker.TickerPrice - tick, err := b.GetTicker(p.Pair().Lower().String()) +// UpdateTicker updates and returns the ticker for a currency pair +func (b *BTCC) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price + tick, err := b.GetTicker(exchange.FormatExchangeCurrency(b.GetName(), p).String()) if err != nil { return tickerPrice, err } - tickerPrice.Pair = p tickerPrice.Ask = tick.Sell tickerPrice.Bid = tick.Buy @@ -63,39 +42,52 @@ func (b *BTCC) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { tickerPrice.Last = tick.Last tickerPrice.Volume = tick.Vol tickerPrice.High = tick.High - ticker.ProcessTicker(b.GetName(), p, tickerPrice) - return tickerPrice, nil + ticker.ProcessTicker(b.GetName(), p, tickerPrice, assetType) + return ticker.GetTicker(b.Name, p, assetType) } -func (b *BTCC) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(b.GetName(), p) - if err == nil { - return ob, nil +// GetTickerPrice returns the ticker for a currency pair +func (b *BTCC) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(b.GetName(), p, assetType) + if err != nil { + return b.UpdateTicker(p, assetType) } + return tickerNew, nil +} - var orderBook orderbook.OrderbookBase - orderbookNew, err := b.GetOrderBook(p.Pair().Lower().String(), 100) +// GetOrderbookEx returns the orderbook for a currency pair +func (b *BTCC) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(b.GetName(), p, assetType) + if err == nil { + return b.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (b *BTCC) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + orderbookNew, err := b.GetOrderBook(exchange.FormatExchangeCurrency(b.GetName(), p).String(), 100) if err != nil { return orderBook, err } for x := range orderbookNew.Bids { data := orderbookNew.Bids[x] - orderBook.Bids = append(ob.Bids, orderbook.OrderbookItem{Price: data[0], Amount: data[1]}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Price: data[0], Amount: data[1]}) } for x := range orderbookNew.Asks { data := orderbookNew.Asks[x] - orderBook.Asks = append(ob.Asks, orderbook.OrderbookItem{Price: data[0], Amount: data[1]}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Price: data[0], Amount: data[1]}) } - orderBook.Pair = p - orderbook.ProcessOrderbook(b.GetName(), p, orderBook) - return orderBook, nil + orderbook.ProcessOrderbook(b.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(b.Name, p, assetType) } -//TODO: Retrieve BTCC info -//GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the Kraken exchange +// GetExchangeAccountInfo : Retrieves balances for all enabled currencies for +// the Kraken exchange - TODO func (b *BTCC) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo response.ExchangeName = b.GetName() diff --git a/exchanges/btce/btce.go b/exchanges/btce/btce.go deleted file mode 100644 index f96819c3..00000000 --- a/exchanges/btce/btce.go +++ /dev/null @@ -1,349 +0,0 @@ -package btce - -import ( - "errors" - "fmt" - "log" - "net/url" - "strconv" - "strings" - "time" - - "github.com/thrasher-/gocryptotrader/common" - "github.com/thrasher-/gocryptotrader/config" - "github.com/thrasher-/gocryptotrader/exchanges" -) - -const ( - BTCE_API_PUBLIC_URL = "https://btc-e.com/api" - BTCE_API_PRIVATE_URL = "https://btc-e.com/tapi" - BTCE_API_PUBLIC_VERSION = "3" - BTCE_API_PRIVATE_VERSION = "1" - BTCE_INFO = "info" - BTCE_TICKER = "ticker" - BTCE_DEPTH = "depth" - BTCE_TRADES = "trades" - BTCE_ACCOUNT_INFO = "getInfo" - BTCE_TRADE = "Trade" - BTCE_ACTIVE_ORDERS = "ActiveOrders" - BTCE_ORDER_INFO = "OrderInfo" - BTCE_CANCEL_ORDER = "CancelOrder" - BTCE_TRADE_HISTORY = "TradeHistory" - BTCE_TRANSACTION_HISTORY = "TransHistory" - BTCE_WITHDRAW_COIN = "WithdrawCoin" - BTCE_CREATE_COUPON = "CreateCoupon" - BTCE_REDEEM_COUPON = "RedeemCoupon" -) - -type BTCE struct { - exchange.Base - Ticker map[string]BTCeTicker -} - -func (b *BTCE) SetDefaults() { - b.Name = "BTCE" - b.Enabled = false - b.Fee = 0.2 - b.Verbose = false - b.Websocket = false - b.RESTPollingDelay = 10 - b.Ticker = make(map[string]BTCeTicker) -} - -func (b *BTCE) Setup(exch config.ExchangeConfig) { - if !exch.Enabled { - b.SetEnabled(false) - } else { - b.Enabled = true - b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - b.RESTPollingDelay = exch.RESTPollingDelay - b.Verbose = exch.Verbose - b.Websocket = exch.Websocket - b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") - b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") - b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") - - } -} - -func (b *BTCE) GetFee() float64 { - return b.Fee -} - -func (b *BTCE) GetInfo() (BTCEInfo, error) { - req := fmt.Sprintf("%s/%s/%s/", BTCE_API_PUBLIC_URL, BTCE_API_PUBLIC_VERSION, BTCE_INFO) - resp := BTCEInfo{} - err := common.SendHTTPGetRequest(req, true, &resp) - - if err != nil { - return resp, err - } - - return resp, nil -} - -func (b *BTCE) GetTicker(symbol string) (map[string]BTCeTicker, error) { - type Response struct { - Data map[string]BTCeTicker - } - - response := Response{} - req := fmt.Sprintf("%s/%s/%s/%s", BTCE_API_PUBLIC_URL, BTCE_API_PUBLIC_VERSION, BTCE_TICKER, symbol) - err := common.SendHTTPGetRequest(req, true, &response.Data) - - if err != nil { - return nil, err - } - return response.Data, nil -} - -func (b *BTCE) GetDepth(symbol string) (BTCEOrderbook, error) { - type Response struct { - Data map[string]BTCEOrderbook - } - - response := Response{} - req := fmt.Sprintf("%s/%s/%s/%s", BTCE_API_PUBLIC_URL, BTCE_API_PUBLIC_VERSION, BTCE_DEPTH, symbol) - - err := common.SendHTTPGetRequest(req, true, &response.Data) - if err != nil { - return BTCEOrderbook{}, err - } - - depth := response.Data[symbol] - return depth, nil -} - -func (b *BTCE) GetTrades(symbol string) ([]BTCETrades, error) { - type Response struct { - Data map[string][]BTCETrades - } - - response := Response{} - req := fmt.Sprintf("%s/%s/%s/%s", BTCE_API_PUBLIC_URL, BTCE_API_PUBLIC_VERSION, BTCE_TRADES, symbol) - - err := common.SendHTTPGetRequest(req, true, &response.Data) - if err != nil { - return []BTCETrades{}, err - } - - trades := response.Data[symbol] - return trades, nil -} - -func (b *BTCE) GetAccountInfo() (BTCEAccountInfo, error) { - var result BTCEAccountInfo - err := b.SendAuthenticatedHTTPRequest(BTCE_ACCOUNT_INFO, url.Values{}, &result) - - if err != nil { - return result, err - } - - return result, nil -} - -func (b *BTCE) GetActiveOrders(pair string) (map[string]BTCEActiveOrders, error) { - req := url.Values{} - req.Add("pair", pair) - - var result map[string]BTCEActiveOrders - err := b.SendAuthenticatedHTTPRequest(BTCE_ACTIVE_ORDERS, req, &result) - - if err != nil { - return result, err - } - - return result, nil -} - -func (b *BTCE) GetOrderInfo(OrderID int64) (map[string]BTCEOrderInfo, error) { - req := url.Values{} - req.Add("order_id", strconv.FormatInt(OrderID, 10)) - - var result map[string]BTCEOrderInfo - err := b.SendAuthenticatedHTTPRequest(BTCE_ORDER_INFO, req, &result) - - if err != nil { - return result, err - } - - return result, nil -} - -func (b *BTCE) CancelOrder(OrderID int64) (bool, error) { - req := url.Values{} - req.Add("order_id", strconv.FormatInt(OrderID, 10)) - - var result BTCECancelOrder - err := b.SendAuthenticatedHTTPRequest(BTCE_CANCEL_ORDER, req, &result) - - if err != nil { - return false, err - } - - return true, nil -} - -//to-do: convert orderid to int64 -func (b *BTCE) Trade(pair, orderType string, amount, price float64) (float64, error) { - req := url.Values{} - req.Add("pair", pair) - req.Add("type", orderType) - req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - req.Add("rate", strconv.FormatFloat(price, 'f', -1, 64)) - - var result BTCETrade - err := b.SendAuthenticatedHTTPRequest(BTCE_TRADE, req, &result) - - if err != nil { - return 0, err - } - - return result.OrderID, nil -} - -func (b *BTCE) GetTransactionHistory(TIDFrom, Count, TIDEnd int64, order, since, end string) (map[string]BTCETransHistory, error) { - req := url.Values{} - req.Add("from", strconv.FormatInt(TIDFrom, 10)) - req.Add("count", strconv.FormatInt(Count, 10)) - req.Add("from_id", strconv.FormatInt(TIDFrom, 10)) - req.Add("end_id", strconv.FormatInt(TIDEnd, 10)) - req.Add("order", order) - req.Add("since", since) - req.Add("end", end) - - var result map[string]BTCETransHistory - err := b.SendAuthenticatedHTTPRequest(BTCE_TRANSACTION_HISTORY, req, &result) - - if err != nil { - return result, err - } - - return result, nil -} - -func (b *BTCE) GetTradeHistory(TIDFrom, Count, TIDEnd int64, order, since, end, pair string) (map[string]BTCETradeHistory, error) { - req := url.Values{} - - req.Add("from", strconv.FormatInt(TIDFrom, 10)) - req.Add("count", strconv.FormatInt(Count, 10)) - req.Add("from_id", strconv.FormatInt(TIDFrom, 10)) - req.Add("end_id", strconv.FormatInt(TIDEnd, 10)) - req.Add("order", order) - req.Add("since", since) - req.Add("end", end) - req.Add("pair", pair) - - var result map[string]BTCETradeHistory - err := b.SendAuthenticatedHTTPRequest(BTCE_TRADE_HISTORY, req, &result) - - if err != nil { - return result, err - } - - return result, nil -} - -func (b *BTCE) WithdrawCoins(coin string, amount float64, address string) (BTCEWithdrawCoins, error) { - req := url.Values{} - - req.Add("coinName", coin) - req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - req.Add("address", address) - - var result BTCEWithdrawCoins - err := b.SendAuthenticatedHTTPRequest(BTCE_WITHDRAW_COIN, req, &result) - - if err != nil { - return result, err - } - return result, nil -} - -func (b *BTCE) CreateCoupon(currency string, amount float64) (BTCECreateCoupon, error) { - req := url.Values{} - - req.Add("currency", currency) - req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - - var result BTCECreateCoupon - err := b.SendAuthenticatedHTTPRequest(BTCE_CREATE_COUPON, req, &result) - - if err != nil { - return result, err - } - - return result, nil -} - -func (b *BTCE) RedeemCoupon(coupon string) (BTCERedeemCoupon, error) { - req := url.Values{} - - req.Add("coupon", coupon) - - var result BTCERedeemCoupon - err := b.SendAuthenticatedHTTPRequest(BTCE_REDEEM_COUPON, req, &result) - - if err != nil { - return result, err - } - - return result, nil -} - -func (b *BTCE) SendAuthenticatedHTTPRequest(method string, values url.Values, result interface{}) (err error) { - if !b.AuthenticatedAPISupport { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) - } - - if b.Nonce.Get() == 0 { - b.Nonce.Set(time.Now().Unix()) - } else { - b.Nonce.Inc() - } - values.Set("nonce", b.Nonce.String()) - values.Set("method", method) - - encoded := values.Encode() - hmac := common.GetHMAC(common.HashSHA512, []byte(encoded), []byte(b.APISecret)) - - if b.Verbose { - log.Printf("Sending POST request to %s calling method %s with params %s\n", BTCE_API_PRIVATE_URL, method, encoded) - } - - headers := make(map[string]string) - headers["Key"] = b.APIKey - headers["Sign"] = common.HexEncodeToString(hmac) - headers["Content-Type"] = "application/x-www-form-urlencoded" - - resp, err := common.SendHTTPRequest("POST", BTCE_API_PRIVATE_URL, headers, strings.NewReader(encoded)) - - if err != nil { - return err - } - - response := BTCEResponse{} - err = common.JSONDecode([]byte(resp), &response) - - if err != nil { - return err - } - - if response.Success != 1 { - return errors.New(response.Error) - } - - JSONEncoded, err := common.JSONEncode(response.Return) - - if err != nil { - return err - } - - err = common.JSONDecode(JSONEncoded, &result) - - if err != nil { - return err - } - return nil -} diff --git a/exchanges/btce/btce_wrapper.go b/exchanges/btce/btce_wrapper.go deleted file mode 100644 index c3d6bed9..00000000 --- a/exchanges/btce/btce_wrapper.go +++ /dev/null @@ -1,114 +0,0 @@ -package btce - -import ( - "errors" - "log" - "time" - - "github.com/thrasher-/gocryptotrader/common" - "github.com/thrasher-/gocryptotrader/currency/pair" - "github.com/thrasher-/gocryptotrader/exchanges" - "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" - "github.com/thrasher-/gocryptotrader/exchanges/ticker" -) - -func (b *BTCE) Start() { - go b.Run() -} - -func (b *BTCE) Run() { - if b.Verbose { - log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket)) - log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) - log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) - } - - pairs := []string{} - for _, x := range b.EnabledPairs { - x = common.StringToLower(x[0:3] + "_" + x[3:6]) - pairs = append(pairs, x) - } - pairsString := common.JoinStrings(pairs, "-") - - for b.Enabled { - go func() { - ticker, err := b.GetTicker(pairsString) - if err != nil { - log.Println(err) - return - } - for x, y := range ticker { - x = common.StringToUpper(x[0:3] + x[4:]) - log.Printf("BTC-e %s: Last %f High %f Low %f Volume %f\n", x, y.Last, y.High, y.Low, y.Vol_cur) - b.Ticker[x] = y - stats.AddExchangeInfo(b.GetName(), common.StringToUpper(x[0:3]), common.StringToUpper(x[4:]), y.Last, y.Vol_cur) - } - }() - time.Sleep(time.Second * b.RESTPollingDelay) - } -} - -func (b *BTCE) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - var tickerPrice ticker.TickerPrice - tick, ok := b.Ticker[p.Pair().Lower().String()] - if !ok { - return tickerPrice, errors.New("Unable to get currency.") - } - tickerPrice.Pair = p - tickerPrice.Ask = tick.Buy - tickerPrice.Bid = tick.Sell - tickerPrice.Low = tick.Low - tickerPrice.Last = tick.Last - tickerPrice.Volume = tick.Vol_cur - tickerPrice.High = tick.High - ticker.ProcessTicker(b.GetName(), p, tickerPrice) - return tickerPrice, nil -} - -func (b *BTCE) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(b.GetName(), p) - if err == nil { - return ob, nil - } - - var orderBook orderbook.OrderbookBase - orderbookNew, err := b.GetDepth(p.Pair().Lower().String()) - if err != nil { - return orderBook, err - } - - for x := range orderbookNew.Bids { - data := orderbookNew.Bids[x] - orderBook.Bids = append(ob.Bids, orderbook.OrderbookItem{Price: data[0], Amount: data[1]}) - } - - for x := range orderbookNew.Asks { - data := orderbookNew.Asks[x] - orderBook.Asks = append(ob.Asks, orderbook.OrderbookItem{Price: data[0], Amount: data[1]}) - } - - orderBook.Pair = p - orderbook.ProcessOrderbook(b.GetName(), p, orderBook) - return orderBook, nil -} - -//GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the BTCE exchange -func (e *BTCE) GetExchangeAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - response.ExchangeName = e.GetName() - accountBalance, err := e.GetAccountInfo() - if err != nil { - return response, err - } - - for x, y := range accountBalance.Funds { - var exchangeCurrency exchange.AccountCurrencyInfo - exchangeCurrency.CurrencyName = common.StringToUpper(x) - exchangeCurrency.TotalValue = y - exchangeCurrency.Hold = 0 - response.Currencies = append(response.Currencies, exchangeCurrency) - } - - return response, nil -} diff --git a/exchanges/btcmarkets/btcmarkets.go b/exchanges/btcmarkets/btcmarkets.go index 10446ed4..1f6bc8e8 100644 --- a/exchanges/btcmarkets/btcmarkets.go +++ b/exchanges/btcmarkets/btcmarkets.go @@ -11,6 +11,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( @@ -52,6 +53,11 @@ func (b *BTCMarkets) SetDefaults() { b.Websocket = false b.RESTPollingDelay = 10 b.Ticker = make(map[string]Ticker) + b.RequestCurrencyPairFormat.Delimiter = "" + b.RequestCurrencyPairFormat.Uppercase = true + b.ConfigCurrencyPairFormat.Delimiter = "" + b.ConfigCurrencyPairFormat.Uppercase = true + b.AssetTypes = []string{ticker.Spot} } // Setup takes in an exchange configuration and sets all paramaters @@ -68,7 +74,14 @@ func (b *BTCMarkets) Setup(exch config.ExchangeConfig) { b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") - + err := b.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = b.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } @@ -84,7 +97,7 @@ func (b *BTCMarkets) GetTicker(symbol string) (Ticker, error) { path := fmt.Sprintf("/market/%s/AUD/tick", common.StringToUpper(symbol)) return ticker, - common.SendHTTPGetRequest(btcMarketsAPIURL+path, true, &ticker) + common.SendHTTPGetRequest(btcMarketsAPIURL+path, true, b.Verbose, &ticker) } // GetOrderbook returns current orderbook @@ -94,7 +107,7 @@ func (b *BTCMarkets) GetOrderbook(symbol string) (Orderbook, error) { path := fmt.Sprintf("/market/%s/AUD/orderbook", common.StringToUpper(symbol)) return orderbook, - common.SendHTTPGetRequest(btcMarketsAPIURL+path, true, &orderbook) + common.SendHTTPGetRequest(btcMarketsAPIURL+path, true, b.Verbose, &orderbook) } // GetTrades returns executed trades on the exchange @@ -104,7 +117,7 @@ func (b *BTCMarkets) GetTrades(symbol string, values url.Values) ([]Trade, error trades := []Trade{} path := common.EncodeURLValues(fmt.Sprintf("%s/market/%s/AUD/trades", btcMarketsAPIURL, symbol), values) - return trades, common.SendHTTPGetRequest(path, true, &trades) + return trades, common.SendHTTPGetRequest(path, true, b.Verbose, &trades) } // NewOrder requests a new order and returns an ID diff --git a/exchanges/btcmarkets/btcmarkets_test.go b/exchanges/btcmarkets/btcmarkets_test.go index ca7ded8b..bc0e9776 100644 --- a/exchanges/btcmarkets/btcmarkets_test.go +++ b/exchanges/btcmarkets/btcmarkets_test.go @@ -3,6 +3,7 @@ package btcmarkets import ( "net/url" "testing" + "time" "github.com/thrasher-/gocryptotrader/config" ) @@ -20,16 +21,31 @@ func TestSetDefaults(t *testing.T) { } func TestSetup(t *testing.T) { - conf := config.ExchangeConfig{} - bm.Setup(conf) - - conf = config.ExchangeConfig{ - APIKey: apiKey, - APISecret: apiSecret, - Enabled: true, - AuthenticatedAPISupport: true, + t.Parallel() + b := BTCMarkets{} + b.Name = "BTC Markets" + cfg := config.GetConfig() + cfg.LoadConfig("../../testdata/configtest.dat") + bConfig, err := cfg.GetExchangeConfig("BTC Markets") + if err != nil { + t.Error("Test Failed - BTC Markets Setup() init error") + } + + b.SetDefaults() + b.Setup(bConfig) + + if !b.IsEnabled() || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) || + b.Verbose || b.Websocket || len(b.BaseCurrencies) < 1 || + len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 { + t.Error("Test Failed - BTC Markets Setup values not set correctly") + } + + bConfig.Enabled = false + b.Setup(bConfig) + + if b.IsEnabled() { + t.Error("Test failed - BTC Markets TestSetup incorrect value") } - bm.Setup(conf) } func TestGetFee(t *testing.T) { diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index cfd09dc9..d9fde312 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -2,56 +2,57 @@ package btcmarkets import ( "log" - "time" - "github.com/thrasher-/gocryptotrader/currency" + "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) -// Start runs ticker monitor in a new routine +// Start starts the BTC Markets go routine func (b *BTCMarkets) Start() { go b.Run() } -// Run starts a go routine to monitor ticker price +// Run implements the BTC Markets wrapper func (b *BTCMarkets) Run() { if b.Verbose { log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) } - for b.Enabled { - for _, x := range b.EnabledPairs { - curr := pair.NewCurrencyPair(x, "AUD") - go func() { - ticker, err := b.GetTickerPrice(curr) - if err != nil { - return - } - BTCMarketsLastUSD, _ := currency.ConvertCurrency(ticker.Last, "AUD", "USD") - BTCMarketsBestBidUSD, _ := currency.ConvertCurrency(ticker.Bid, "AUD", "USD") - BTCMarketsBestAskUSD, _ := currency.ConvertCurrency(ticker.Ask, "AUD", "USD") - log.Printf("BTC Markets %s: Last %f (%f) Bid %f (%f) Ask %f (%f)\n", exchange.FormatCurrency(curr).String(), BTCMarketsLastUSD, ticker.Last, BTCMarketsBestBidUSD, ticker.Bid, BTCMarketsBestAskUSD, ticker.Ask) - stats.AddExchangeInfo(b.GetName(), curr.GetFirstCurrency().String(), curr.GetSecondCurrency().String(), ticker.Last, 0) - stats.AddExchangeInfo(b.GetName(), curr.GetFirstCurrency().String(), "USD", BTCMarketsLastUSD, 0) - }() + if !common.DataContains(b.EnabledPairs, "AUD") || !common.DataContains(b.EnabledPairs, "AUD") { + enabledPairs := []string{} + for x := range b.EnabledPairs { + enabledPairs = append(enabledPairs, b.EnabledPairs[x]+"AUD") + } + + availablePairs := []string{} + for x := range b.AvailablePairs { + availablePairs = append(availablePairs, b.AvailablePairs[x]+"AUD") + } + + log.Println("BTCMarkets: Upgrading available and enabled pairs") + + err := b.UpdateEnabledCurrencies(enabledPairs, true) + if err != nil { + log.Printf("%s Failed to get config.\n", b.GetName()) + return + } + + err = b.UpdateAvailableCurrencies(availablePairs, true) + if err != nil { + log.Printf("%s Failed to get config.\n", b.GetName()) + return } - time.Sleep(time.Second * b.RESTPollingDelay) } } -// GetTickerPrice returns ticker information -func (b *BTCMarkets) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - tickerNew, err := ticker.GetTicker(b.GetName(), p) - if err == nil { - return tickerNew, nil - } - - var tickerPrice ticker.TickerPrice +// UpdateTicker updates and returns the ticker for a currency pair +func (b *BTCMarkets) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price tick, err := b.GetTicker(p.GetFirstCurrency().String()) if err != nil { return tickerPrice, err @@ -60,18 +61,31 @@ func (b *BTCMarkets) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, er tickerPrice.Ask = tick.BestAsk tickerPrice.Bid = tick.BestBID tickerPrice.Last = tick.LastPrice - ticker.ProcessTicker(b.GetName(), p, tickerPrice) - return tickerPrice, nil + ticker.ProcessTicker(b.GetName(), p, tickerPrice, assetType) + return ticker.GetTicker(b.Name, p, assetType) +} + +// GetTickerPrice returns the ticker for a currency pair +func (b *BTCMarkets) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(b.GetName(), p, assetType) + if err != nil { + return b.UpdateTicker(p, assetType) + } + return tickerNew, nil } // GetOrderbookEx returns orderbook base on the currency pair -func (b *BTCMarkets) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(b.GetName(), p) +func (b *BTCMarkets) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(b.GetName(), p, assetType) if err == nil { - return ob, nil + return b.UpdateOrderbook(p, assetType) } + return ob, nil +} - var orderBook orderbook.OrderbookBase +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (b *BTCMarkets) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base orderbookNew, err := b.GetOrderbook(p.GetFirstCurrency().String()) if err != nil { return orderBook, err @@ -79,17 +93,16 @@ func (b *BTCMarkets) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBas for x := range orderbookNew.Bids { data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.OrderbookItem{Amount: data[1], Price: data[0]}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data[1], Price: data[0]}) } for x := range orderbookNew.Asks { data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.OrderbookItem{Amount: data[1], Price: data[0]}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data[1], Price: data[0]}) } - orderBook.Pair = p - orderbook.ProcessOrderbook(b.GetName(), p, orderBook) - return orderBook, nil + orderbook.ProcessOrderbook(b.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(b.Name, p, assetType) } // GetExchangeAccountInfo retrieves balances for all enabled currencies for the diff --git a/exchanges/coinut/coinut.go b/exchanges/coinut/coinut.go index 5d262881..969f74ec 100644 --- a/exchanges/coinut/coinut.go +++ b/exchanges/coinut/coinut.go @@ -11,34 +11,37 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( - COINUT_API_URL = "https://api.coinut.com" - COINUT_API_VERSION = "1" - COINUT_INSTRUMENTS = "inst_list" - COINUT_TICKER = "inst_tick" - COINUT_ORDERBOOK = "inst_order_book" - COINUT_TRADES = "inst_trade" - COINUT_BALANCE = "user_balance" - COINUT_ORDER = "new_order" - COINUT_ORDERS = "new_orders" - COINUT_ORDERS_OPEN = "user_open_orders" - COINUT_ORDER_CANCEL = "cancel_order" - COINUT_ORDERS_CANCEL = "cancel_orders" - COINUT_TRADE_HISTORY = "trade_history" - COINUT_INDEX_TICKER = "index_tick" - COINUT_OPTION_CHAIN = "option_chain" - COINUT_POSITION_HISTORY = "position_history" - COINUT_POSITION_OPEN = "user_open_positions" + coinutAPIURL = "https://api.coinut.com" + coinutAPIVersion = "1" + coinutInstruments = "inst_list" + coinutTicker = "inst_tick" + coinutOrderbook = "inst_order_book" + coinutTrades = "inst_trade" + coinutBalance = "user_balance" + coinutOrder = "new_order" + coinutOrders = "new_orders" + coinutOrdersOpen = "user_open_orders" + coinutOrderCancel = "cancel_order" + coinutOrdersCancel = "cancel_orders" + coinutTradeHistory = "trade_history" + coinutIndexTicker = "index_tick" + coinutOptionChain = "option_chain" + coinutPositionHistory = "position_history" + coinutPositionOpen = "user_open_positions" ) +// COINUT is the overarching type across the coinut package type COINUT struct { exchange.Base WebsocketConn *websocket.Conn InstrumentMap map[string]int } +// SetDefaults sets current default values func (c *COINUT) SetDefaults() { c.Name = "COINUT" c.Enabled = false @@ -48,8 +51,14 @@ func (c *COINUT) SetDefaults() { c.Verbose = false c.Websocket = false c.RESTPollingDelay = 10 + c.RequestCurrencyPairFormat.Delimiter = "" + c.RequestCurrencyPairFormat.Uppercase = true + c.ConfigCurrencyPairFormat.Delimiter = "" + c.ConfigCurrencyPairFormat.Uppercase = true + c.AssetTypes = []string{ticker.Spot} } +// Setup sets the current exchange configuration func (c *COINUT) Setup(exch config.ExchangeConfig) { if !exch.Enabled { c.SetEnabled(false) @@ -63,14 +72,23 @@ func (c *COINUT) Setup(exch config.ExchangeConfig) { c.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") c.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") c.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := c.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = c.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } +// GetInstruments returns instruments func (c *COINUT) GetInstruments() (CoinutInstruments, error) { var result CoinutInstruments params := make(map[string]interface{}) params["sec_type"] = "SPOT" - err := c.SendAuthenticatedHTTPRequest(COINUT_INSTRUMENTS, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutInstruments, params, &result) if err != nil { return result, err } @@ -81,7 +99,7 @@ func (c *COINUT) GetInstrumentTicker(instrumentID int) (CoinutTicker, error) { var result CoinutTicker params := make(map[string]interface{}) params["inst_id"] = instrumentID - err := c.SendAuthenticatedHTTPRequest(COINUT_TICKER, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutTicker, params, &result) if err != nil { return result, err } @@ -95,7 +113,7 @@ func (c *COINUT) GetInstrumentOrderbook(instrumentID, limit int) (CoinutOrderboo if limit > 0 { params["top_n"] = limit } - err := c.SendAuthenticatedHTTPRequest(COINUT_ORDERBOOK, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutOrderbook, params, &result) if err != nil { return result, err } @@ -106,7 +124,7 @@ func (c *COINUT) GetTrades(instrumentID int) (CoinutTrades, error) { var result CoinutTrades params := make(map[string]interface{}) params["inst_id"] = instrumentID - err := c.SendAuthenticatedHTTPRequest(COINUT_TRADES, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutTrades, params, &result) if err != nil { return result, err } @@ -115,7 +133,7 @@ func (c *COINUT) GetTrades(instrumentID int) (CoinutTrades, error) { func (c *COINUT) GetUserBalance() (CoinutUserBalance, error) { result := CoinutUserBalance{} - err := c.SendAuthenticatedHTTPRequest(COINUT_BALANCE, nil, &result) + err := c.SendAuthenticatedHTTPRequest(coinutBalance, nil, &result) if err != nil { return result, err } @@ -134,7 +152,7 @@ func (c *COINUT) NewOrder(instrumentID int, quantity, price float64, buy bool, o } params["client_ord_id"] = orderID - err := c.SendAuthenticatedHTTPRequest(COINUT_ORDER, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutOrder, params, &result) if err != nil { return result, err } @@ -145,7 +163,7 @@ func (c *COINUT) NewOrders(orders []CoinutOrder) ([]CoinutOrdersBase, error) { var result CoinutOrdersResponse params := make(map[string]interface{}) params["orders"] = orders - err := c.SendAuthenticatedHTTPRequest(COINUT_ORDERS, params, &result.Data) + err := c.SendAuthenticatedHTTPRequest(coinutOrders, params, &result.Data) if err != nil { return nil, err } @@ -156,7 +174,7 @@ func (c *COINUT) GetOpenOrders(instrumentID int) ([]CoinutOrdersResponse, error) var result []CoinutOrdersResponse params := make(map[string]interface{}) params["inst_id"] = instrumentID - err := c.SendAuthenticatedHTTPRequest(COINUT_ORDERS_OPEN, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutOrdersOpen, params, &result) if err != nil { return nil, err } @@ -168,7 +186,7 @@ func (c *COINUT) CancelOrder(instrumentID, orderID int) (bool, error) { params := make(map[string]interface{}) params["inst_id"] = instrumentID params["order_id"] = orderID - err := c.SendAuthenticatedHTTPRequest(COINUT_ORDERS_CANCEL, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutOrdersCancel, params, &result) if err != nil { return false, err } @@ -179,7 +197,7 @@ func (c *COINUT) CancelOrders(orders []CoinutCancelOrders) (CoinutCancelOrdersRe var result CoinutCancelOrdersResponse params := make(map[string]interface{}) params["entries"] = orders - err := c.SendAuthenticatedHTTPRequest(COINUT_ORDERS_CANCEL, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutOrdersCancel, params, &result) if err != nil { return result, err } @@ -196,7 +214,7 @@ func (c *COINUT) GetTradeHistory(instrumentID, start, limit int) (CoinutTradeHis if limit >= 0 && start <= 100 { params["limit"] = limit } - err := c.SendAuthenticatedHTTPRequest(COINUT_TRADE_HISTORY, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutTradeHistory, params, &result) if err != nil { return result, err } @@ -207,7 +225,7 @@ func (c *COINUT) GetIndexTicker(asset string) (CoinutIndexTicker, error) { var result CoinutIndexTicker params := make(map[string]interface{}) params["asset"] = asset - err := c.SendAuthenticatedHTTPRequest(COINUT_INDEX_TICKER, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutIndexTicker, params, &result) if err != nil { return result, err } @@ -218,7 +236,7 @@ func (c *COINUT) GetDerivativeInstruments(secType string) (interface{}, error) { var result interface{} //to-do params := make(map[string]interface{}) params["sec_type"] = secType - err := c.SendAuthenticatedHTTPRequest(COINUT_INSTRUMENTS, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutInstruments, params, &result) if err != nil { return result, err } @@ -230,7 +248,7 @@ func (c *COINUT) GetOptionChain(asset, secType string, expiry int64) (CoinutOpti params := make(map[string]interface{}) params["asset"] = asset params["sec_type"] = secType - err := c.SendAuthenticatedHTTPRequest(COINUT_OPTION_CHAIN, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutOptionChain, params, &result) if err != nil { return result, err } @@ -247,7 +265,7 @@ func (c *COINUT) GetPositionHistory(secType string, start, limit int) (CoinutPos if limit >= 0 { params["limit"] = limit } - err := c.SendAuthenticatedHTTPRequest(COINUT_POSITION_HISTORY, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutPositionHistory, params, &result) if err != nil { return result, err } @@ -262,7 +280,7 @@ func (c *COINUT) GetOpenPosition(instrumentID int) ([]CoinutOpenPosition, error) params := make(map[string]interface{}) params["inst_id"] = instrumentID - err := c.SendAuthenticatedHTTPRequest(COINUT_POSITION_OPEN, params, &result) + err := c.SendAuthenticatedHTTPRequest(coinutPositionOpen, params, &result) if err != nil { return result.Positions, err } @@ -305,7 +323,10 @@ func (c *COINUT) SendAuthenticatedHTTPRequest(apiRequest string, params map[stri headers["X-SIGNATURE"] = common.HexEncodeToString(hmac) headers["Content-Type"] = "application/json" - resp, err := common.SendHTTPRequest("POST", COINUT_API_URL, headers, bytes.NewBuffer(payload)) + resp, err := common.SendHTTPRequest("POST", coinutAPIURL, headers, bytes.NewBuffer(payload)) + if err != nil { + return err + } if c.Verbose { log.Printf("Received raw: \n%s", resp) diff --git a/exchanges/coinut/coinut_test.go b/exchanges/coinut/coinut_test.go new file mode 100644 index 00000000..9fead096 --- /dev/null +++ b/exchanges/coinut/coinut_test.go @@ -0,0 +1,32 @@ +package coinut + +// +// const ( +// apiKey = "" +// apiSecret = "" +// ) +// +// var c COINUT +// +// func TestSetDefaults(t *testing.T) { +// c.SetDefaults() +// } +// +// func TestSetup(t *testing.T) { +// exch := config.ExchangeConfig{} +// c.Setup(exch) +// +// exch.Enabled = true +// exch.APIKey = apiKey +// exch.APISecret = apiSecret +// c.Setup(exch) +// } +// +// // func TestGetInstruments(t *testing.T) { +// // c.Verbose = true +// // resp, err := c.GetInstruments() +// // if err == nil { +// // t.Error("Test failed - GetInstruments() error", err) +// // } +// // log.Println(resp) +// // } diff --git a/exchanges/coinut/coinut_types.go b/exchanges/coinut/coinut_types.go index 91b55223..10db1bf9 100644 --- a/exchanges/coinut/coinut_types.go +++ b/exchanges/coinut/coinut_types.go @@ -34,7 +34,7 @@ type CoinutTicker struct { type CoinutOrderbookBase struct { Count int `json:"count"` Price float64 `json:"price,string"` - Quantity float64 `json:"quantity,string"` + Quantity float64 `json:"qty,string"` } type CoinutOrderbook struct { diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 66dcd242..0035e049 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -2,20 +2,20 @@ package coinut import ( "log" - "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +// Start starts the COINUT go routine func (c *COINUT) Start() { go c.Run() } +// Run implements the COINUT wrapper func (c *COINUT) Run() { if c.Verbose { log.Printf("%s Websocket: %s. (url: %s).\n", c.GetName(), common.IsEnabled(c.Websocket), COINUT_WEBSOCKET_URL) @@ -40,30 +40,15 @@ func (c *COINUT) Run() { currencies = append(currencies, x) } - err = c.UpdateAvailableCurrencies(currencies) + err = c.UpdateAvailableCurrencies(currencies, false) if err != nil { log.Printf("%s Failed to get config.\n", c.GetName()) } - - for c.Enabled { - for _, x := range c.EnabledPairs { - currency := pair.NewCurrencyPair(x[0:3], x[3:]) - go func() { - ticker, err := c.GetTickerPrice(currency) - if err != nil { - log.Println(err) - return - } - log.Printf("COINUT %s: Last %f High %f Low %f Volume %f\n", exchange.FormatCurrency(currency).String(), ticker.Last, ticker.High, ticker.Low, ticker.Volume) - stats.AddExchangeInfo(c.GetName(), currency.GetFirstCurrency().String(), currency.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - }() - } - time.Sleep(time.Second * c.RESTPollingDelay) - } } -//GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the COINUT exchange -func (e *COINUT) GetExchangeAccountInfo() (exchange.AccountInfo, error) { +// GetExchangeAccountInfo retrieves balances for all enabled currencies for the +// COINUT exchange +func (c *COINUT) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo /* response.ExchangeName = e.GetName() @@ -83,16 +68,12 @@ func (e *COINUT) GetExchangeAccountInfo() (exchange.AccountInfo, error) { return response, nil } -func (c *COINUT) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - tickerNew, err := ticker.GetTicker(c.GetName(), p) - if err == nil { - return tickerNew, nil - } - - var tickerPrice ticker.TickerPrice +// UpdateTicker updates and returns the ticker for a currency pair +func (c *COINUT) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price tick, err := c.GetInstrumentTicker(c.InstrumentMap[p.Pair().String()]) if err != nil { - return ticker.TickerPrice{}, err + return ticker.Price{}, err } tickerPrice.Pair = p @@ -100,30 +81,45 @@ func (c *COINUT) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) tickerPrice.Last = tick.Last tickerPrice.High = tick.HighestBuy tickerPrice.Low = tick.LowestSell - ticker.ProcessTicker(c.GetName(), p, tickerPrice) - return tickerPrice, nil + ticker.ProcessTicker(c.GetName(), p, tickerPrice, assetType) + return ticker.GetTicker(c.Name, p, assetType) + } -func (c *COINUT) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(c.GetName(), p) - if err == nil { - return ob, nil +// GetTickerPrice returns the ticker for a currency pair +func (c *COINUT) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(c.GetName(), p, assetType) + if err != nil { + return c.UpdateTicker(p, assetType) } + return tickerNew, nil +} - var orderBook orderbook.OrderbookBase +// GetOrderbookEx returns orderbook base on the currency pair +func (c *COINUT) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(c.GetName(), p, assetType) + if err == nil { + return c.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (c *COINUT) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base orderbookNew, err := c.GetInstrumentOrderbook(c.InstrumentMap[p.Pair().String()], 200) if err != nil { return orderBook, err } for x := range orderbookNew.Buy { - orderBook.Bids = append(orderBook.Bids, orderbook.OrderbookItem{Amount: orderbookNew.Buy[x].Quantity, Price: orderbookNew.Buy[x].Price}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: orderbookNew.Buy[x].Quantity, Price: orderbookNew.Buy[x].Price}) } for x := range orderbookNew.Sell { - orderBook.Asks = append(orderBook.Asks, orderbook.OrderbookItem{Amount: orderbookNew.Sell[x].Quantity, Price: orderbookNew.Sell[x].Price}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: orderbookNew.Sell[x].Quantity, Price: orderbookNew.Sell[x].Price}) } - orderBook.Pair = p - orderbook.ProcessOrderbook(c.GetName(), p, orderBook) - return orderBook, nil + + orderbook.ProcessOrderbook(c.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(c.Name, p, assetType) } diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 3eb08590..f66e382b 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -49,8 +49,11 @@ type Base struct { BaseCurrencies []string AvailablePairs []string EnabledPairs []string + AssetTypes []string WebsocketURL string APIUrl string + RequestCurrencyPairFormat config.CurrencyPairFormatConfig + ConfigCurrencyPairFormat config.CurrencyPairFormatConfig } // IBotExchange enforces standard functions for all exchanges supported in @@ -61,13 +64,91 @@ type IBotExchange interface { SetDefaults() GetName() string IsEnabled() bool - GetTickerPrice(currency pair.CurrencyPair) (ticker.TickerPrice, error) - GetOrderbookEx(currency pair.CurrencyPair) (orderbook.OrderbookBase, error) - GetEnabledCurrencies() []string + GetTickerPrice(currency pair.CurrencyPair, assetType string) (ticker.Price, error) + UpdateTicker(currency pair.CurrencyPair, assetType string) (ticker.Price, error) + GetOrderbookEx(currency pair.CurrencyPair, assetType string) (orderbook.Base, error) + UpdateOrderbook(currency pair.CurrencyPair, assetType string) (orderbook.Base, error) + GetEnabledCurrencies() []pair.CurrencyPair GetExchangeAccountInfo() (AccountInfo, error) GetAuthenticatedAPISupport() bool } +// SetAssetTypes checks the exchange asset types (whether it supports SPOT, +// Binary or Futures) and sets it to a default setting if it doesn't exist +func (e *Base) SetAssetTypes() error { + cfg := config.GetConfig() + exch, err := cfg.GetExchangeConfig(e.Name) + if err != nil { + return err + } + + update := false + if exch.AssetTypes == "" { + exch.AssetTypes = common.JoinStrings(e.AssetTypes, ",") + update = true + } else { + e.AssetTypes = common.SplitStrings(exch.AssetTypes, ",") + } + + if update { + return cfg.UpdateExchangeConfig(exch) + } + + return nil +} + +// GetExchangeAssetTypes returns the asset types the exchange supports (SPOT, +// binary, futures) +func GetExchangeAssetTypes(exchName string) ([]string, error) { + cfg := config.GetConfig() + exch, err := cfg.GetExchangeConfig(exchName) + if err != nil { + return nil, err + } + + return common.SplitStrings(exch.AssetTypes, ","), nil +} + +// SetCurrencyPairFormat checks the exchange request and config currency pair +// formats and sets it to a default setting if it doesn't exist +func (e *Base) SetCurrencyPairFormat() error { + cfg := config.GetConfig() + exch, err := cfg.GetExchangeConfig(e.Name) + if err != nil { + return err + } + + update := false + if exch.RequestCurrencyPairFormat == nil { + exch.RequestCurrencyPairFormat = &config.CurrencyPairFormatConfig{ + Delimiter: e.RequestCurrencyPairFormat.Delimiter, + Uppercase: e.RequestCurrencyPairFormat.Uppercase, + Separator: e.RequestCurrencyPairFormat.Separator, + Index: e.RequestCurrencyPairFormat.Index, + } + update = true + } else { + e.RequestCurrencyPairFormat = *exch.RequestCurrencyPairFormat + } + + if exch.ConfigCurrencyPairFormat == nil { + exch.ConfigCurrencyPairFormat = &config.CurrencyPairFormatConfig{ + Delimiter: e.ConfigCurrencyPairFormat.Delimiter, + Uppercase: e.ConfigCurrencyPairFormat.Uppercase, + Separator: e.ConfigCurrencyPairFormat.Separator, + Index: e.ConfigCurrencyPairFormat.Index, + } + update = true + } else { + e.ConfigCurrencyPairFormat = *exch.ConfigCurrencyPairFormat + } + + if update { + return cfg.UpdateExchangeConfig(exch) + } + return nil +} + // GetAuthenticatedAPISupport returns whether the exchange supports // authenticated API requests func (e *Base) GetAuthenticatedAPISupport() bool { @@ -81,14 +162,135 @@ func (e *Base) GetName() string { // GetEnabledCurrencies is a method that returns the enabled currency pairs of // the exchange base -func (e *Base) GetEnabledCurrencies() []string { - return e.EnabledPairs +func (e *Base) GetEnabledCurrencies() []pair.CurrencyPair { + var pairs []pair.CurrencyPair + for x := range e.EnabledPairs { + var currencyPair pair.CurrencyPair + if e.RequestCurrencyPairFormat.Delimiter != "" { + if e.ConfigCurrencyPairFormat.Delimiter != "" { + if e.ConfigCurrencyPairFormat.Delimiter == e.RequestCurrencyPairFormat.Delimiter { + currencyPair = pair.NewCurrencyPairDelimiter(e.EnabledPairs[x], + e.RequestCurrencyPairFormat.Delimiter) + } else { + currencyPair = pair.NewCurrencyPairDelimiter(e.EnabledPairs[x], + e.ConfigCurrencyPairFormat.Delimiter) + currencyPair.Delimiter = "-" + } + } else { + if e.ConfigCurrencyPairFormat.Index != "" { + currencyPair = pair.NewCurrencyPairFromIndex(e.EnabledPairs[x], + e.ConfigCurrencyPairFormat.Index) + } else { + currencyPair = pair.NewCurrencyPair(e.EnabledPairs[x][0:3], + e.EnabledPairs[x][3:]) + } + } + } else { + if e.ConfigCurrencyPairFormat.Delimiter != "" { + currencyPair = pair.NewCurrencyPairDelimiter(e.EnabledPairs[x], + e.ConfigCurrencyPairFormat.Delimiter) + } else { + if e.ConfigCurrencyPairFormat.Index != "" { + currencyPair = pair.NewCurrencyPairFromIndex(e.EnabledPairs[x], + e.ConfigCurrencyPairFormat.Index) + } else { + currencyPair = pair.NewCurrencyPair(e.EnabledPairs[x][0:3], + e.EnabledPairs[x][3:]) + } + } + } + pairs = append(pairs, currencyPair) + } + return pairs } // GetAvailableCurrencies is a method that returns the available currency pairs // of the exchange base -func (e *Base) GetAvailableCurrencies() []string { - return e.AvailablePairs +func (e *Base) GetAvailableCurrencies() []pair.CurrencyPair { + var pairs []pair.CurrencyPair + for x := range e.AvailablePairs { + var currencyPair pair.CurrencyPair + if e.RequestCurrencyPairFormat.Delimiter != "" { + if e.ConfigCurrencyPairFormat.Delimiter != "" { + if e.ConfigCurrencyPairFormat.Delimiter == e.RequestCurrencyPairFormat.Delimiter { + currencyPair = pair.NewCurrencyPairDelimiter(e.AvailablePairs[x], + e.RequestCurrencyPairFormat.Delimiter) + } else { + currencyPair = pair.NewCurrencyPairDelimiter(e.AvailablePairs[x], + e.ConfigCurrencyPairFormat.Delimiter) + currencyPair.Delimiter = "-" + } + } else { + if e.ConfigCurrencyPairFormat.Index != "" { + currencyPair = pair.NewCurrencyPairFromIndex(e.AvailablePairs[x], + e.ConfigCurrencyPairFormat.Index) + } else { + currencyPair = pair.NewCurrencyPair(e.AvailablePairs[x][0:3], + e.AvailablePairs[x][3:]) + } + } + } else { + if e.ConfigCurrencyPairFormat.Delimiter != "" { + currencyPair = pair.NewCurrencyPairDelimiter(e.AvailablePairs[x], + e.ConfigCurrencyPairFormat.Delimiter) + } else { + if e.ConfigCurrencyPairFormat.Index != "" { + currencyPair = pair.NewCurrencyPairFromIndex(e.AvailablePairs[x], + e.ConfigCurrencyPairFormat.Index) + } else { + currencyPair = pair.NewCurrencyPair(e.AvailablePairs[x][0:3], + e.AvailablePairs[x][3:]) + } + } + } + pairs = append(pairs, currencyPair) + } + return pairs +} + +// GetExchangeFormatCurrencySeperator returns whether or not a specific +// exchange contains a separator used for API requests +func GetExchangeFormatCurrencySeperator(exchName string) bool { + cfg := config.GetConfig() + exch, err := cfg.GetExchangeConfig(exchName) + if err != nil { + return false + } + + if exch.RequestCurrencyPairFormat.Separator != "" { + return true + } + return false +} + +// GetAndFormatExchangeCurrencies returns a pair.CurrencyItem string containing +// the exchanges formatted currency pairs +func GetAndFormatExchangeCurrencies(exchName string, pairs []pair.CurrencyPair) (pair.CurrencyItem, error) { + var currencyItems pair.CurrencyItem + cfg := config.GetConfig() + exch, err := cfg.GetExchangeConfig(exchName) + if err != nil { + return currencyItems, err + } + + for x := range pairs { + currencyItems += FormatExchangeCurrency(exchName, pairs[x]) + if x == len(pairs)-1 { + continue + } + currencyItems += pair.CurrencyItem(exch.RequestCurrencyPairFormat.Separator) + } + return currencyItems, nil +} + +// FormatExchangeCurrency is a method that formats and returns a currency pair +// based on the user currency display preferences +func FormatExchangeCurrency(exchName string, p pair.CurrencyPair) pair.CurrencyItem { + cfg := config.GetConfig() + exch, _ := cfg.GetExchangeConfig(exchName) + + return p.Display(exch.RequestCurrencyPairFormat.Delimiter, + exch.RequestCurrencyPairFormat.Uppercase) } // FormatCurrency is a method that formats and returns a currency pair @@ -111,6 +313,10 @@ func (e *Base) IsEnabled() bool { // SetAPIKeys is a method that sets the current API keys for the exchange func (e *Base) SetAPIKeys(APIKey, APISecret, ClientID string, b64Decode bool) { + if !e.AuthenticatedAPISupport { + return + } + e.APIKey = APIKey e.ClientID = ClientID @@ -126,20 +332,50 @@ func (e *Base) SetAPIKeys(APIKey, APISecret, ClientID string, b64Decode bool) { } } -// UpdateAvailableCurrencies is a method that sets new pairs to the current -// exchange -func (e *Base) UpdateAvailableCurrencies(exchangeProducts []string) error { +// UpdateEnabledCurrencies is a method that sets new pairs to the current +// exchange. Setting force to true upgrades the enabled currencies +func (e *Base) UpdateEnabledCurrencies(exchangeProducts []string, force bool) error { exchangeProducts = common.SplitStrings(common.StringToUpper(common.JoinStrings(exchangeProducts, ",")), ",") - diff := common.StringSliceDifference(e.AvailablePairs, exchangeProducts) - if len(diff) > 0 { + diff := common.StringSliceDifference(e.EnabledPairs, exchangeProducts) + if force || len(diff) > 0 { cfg := config.GetConfig() exch, err := cfg.GetExchangeConfig(e.Name) if err != nil { return err } - log.Printf("%s Updating available pairs. Difference: %s.\n", e.Name, diff) - exch.AvailablePairs = common.JoinStrings(exchangeProducts, ",") - cfg.UpdateExchangeConfig(exch) + + if force { + log.Printf("%s forced update of enabled pairs.", e.Name) + } else { + log.Printf("%s Updating available pairs. Difference: %s.\n", e.Name, diff) + } + exch.EnabledPairs = common.JoinStrings(exchangeProducts, ",") + e.EnabledPairs = exchangeProducts + return cfg.UpdateExchangeConfig(exch) + } + return nil +} + +// UpdateAvailableCurrencies is a method that sets new pairs to the current +// exchange. Setting force to true upgrades the available currencies +func (e *Base) UpdateAvailableCurrencies(exchangeProducts []string, force bool) error { + exchangeProducts = common.SplitStrings(common.StringToUpper(common.JoinStrings(exchangeProducts, ",")), ",") + diff := common.StringSliceDifference(e.AvailablePairs, exchangeProducts) + if force || len(diff) > 0 { + cfg := config.GetConfig() + exch, err := cfg.GetExchangeConfig(e.Name) + if err != nil { + return err + } + + if force { + log.Printf("%s forced update of available pairs.", e.Name) + } else { + log.Printf("%s Updating available pairs. Difference: %s.\n", e.Name, diff) + } + exch.AvailablePairs = common.JoinStrings(exchangeProducts, ",") + e.AvailablePairs = exchangeProducts + return cfg.UpdateExchangeConfig(exch) } return nil } diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index bc17f371..2e5e71d1 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -3,11 +3,157 @@ package exchange import ( "testing" - "github.com/thrasher-/gocryptotrader/currency/pair" - + "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" + "github.com/thrasher-/gocryptotrader/currency/pair" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +func TestSetAssetTypes(t *testing.T) { + cfg := config.GetConfig() + err := cfg.LoadConfig(config.ConfigTestFile) + if err != nil { + t.Fatalf("Test failed. TestSetAssetTypes failed to load config file. Error: %s", err) + } + + b := Base{ + Name: "TESTNAME", + } + + err = b.SetAssetTypes() + if err == nil { + t.Fatal("Test failed. TestSetAssetTypes returned nil error for a non-existant exchange") + } + + b.Name = "ANX" + err = b.SetAssetTypes() + if err != nil { + t.Fatalf("Test failed. TestSetAssetTypes. Error %s", err) + } + + exch, err := cfg.GetExchangeConfig(b.Name) + if err != nil { + t.Fatalf("Test failed. TestSetAssetTypes load config failed. Error %s", err) + } + + exch.AssetTypes = "" + err = cfg.UpdateExchangeConfig(exch) + if err != nil { + t.Fatalf("Test failed. TestSetAssetTypes update config failed. Error %s", err) + } + + exch, err = cfg.GetExchangeConfig(b.Name) + if err != nil { + t.Fatalf("Test failed. TestSetAssetTypes load config failed. Error %s", err) + } + + if exch.AssetTypes != "" { + t.Fatal("Test failed. TestSetAssetTypes assetTypes != ''") + } + + err = b.SetAssetTypes() + if err != nil { + t.Fatalf("Test failed. TestSetAssetTypes. Error %s", err) + } + + if !common.DataContains(b.AssetTypes, ticker.Spot) { + t.Fatal("Test failed. TestSetAssetTypes assetTypes is not set") + } +} + +func TestGetExchangeAssetTypes(t *testing.T) { + cfg := config.GetConfig() + err := cfg.LoadConfig(config.ConfigTestFile) + if err != nil { + t.Fatalf("Failed to load config file. Error: %s", err) + } + + result, err := GetExchangeAssetTypes("Bitfinex") + if err != nil { + t.Fatal("Test failed. Unable to obtain Bitfinex asset types") + } + + if !common.DataContains(result, ticker.Spot) { + t.Fatal("Test failed. Bitfinex does not contain default asset type 'SPOT'") + } + + _, err = GetExchangeAssetTypes("non-existant-exchange") + if err == nil { + t.Fatal("Test failed. Got asset types for non-existant exchange") + } +} + +func TestSetCurrencyPairFormat(t *testing.T) { + cfg := config.GetConfig() + err := cfg.LoadConfig(config.ConfigTestFile) + if err != nil { + t.Fatalf("Test failed. TestSetCurrencyPairFormat failed to load config file. Error: %s", err) + } + + b := Base{ + Name: "TESTNAME", + } + + err = b.SetCurrencyPairFormat() + if err == nil { + t.Fatal("Test failed. TestSetCurrencyPairFormat returned nil error for a non-existant exchange") + } + + b.Name = "ANX" + err = b.SetCurrencyPairFormat() + if err != nil { + t.Fatalf("Test failed. TestSetCurrencyPairFormat. Error %s", err) + } + + exch, err := cfg.GetExchangeConfig(b.Name) + if err != nil { + t.Fatalf("Test failed. TestSetCurrencyPairFormat load config failed. Error %s", err) + } + + exch.ConfigCurrencyPairFormat = nil + exch.RequestCurrencyPairFormat = nil + err = cfg.UpdateExchangeConfig(exch) + if err != nil { + t.Fatalf("Test failed. TestSetCurrencyPairFormat update config failed. Error %s", err) + } + + exch, err = cfg.GetExchangeConfig(b.Name) + if err != nil { + t.Fatalf("Test failed. TestSetCurrencyPairFormat load config failed. Error %s", err) + } + + if exch.ConfigCurrencyPairFormat != nil && exch.RequestCurrencyPairFormat != nil { + t.Fatal("Test failed. TestSetCurrencyPairFormat exch values are not nil") + } + + err = b.SetCurrencyPairFormat() + if err != nil { + t.Fatalf("Test failed. TestSetCurrencyPairFormat. Error %s", err) + } + + if b.ConfigCurrencyPairFormat.Delimiter != "" && + b.ConfigCurrencyPairFormat.Index != "BTC" && + b.ConfigCurrencyPairFormat.Uppercase { + t.Fatal("Test failed. TestSetCurrencyPairFormat ConfigCurrencyPairFormat values are incorrect") + } + + if b.RequestCurrencyPairFormat.Delimiter != "" && + b.RequestCurrencyPairFormat.Index != "BTC" && + b.RequestCurrencyPairFormat.Uppercase { + t.Fatal("Test failed. TestSetCurrencyPairFormat RequestCurrencyPairFormat values are incorrect") + } +} + +func TestGetAuthenticatedAPISupport(t *testing.T) { + base := Base{ + AuthenticatedAPISupport: false, + } + + if base.GetAuthenticatedAPISupport() { + t.Fatal("Test failed. TestGetAuthenticatedAPISupport returned true when it should of been false.") + } +} + func TestGetName(t *testing.T) { GetName := Base{ Name: "TESTNAME", @@ -20,29 +166,210 @@ func TestGetName(t *testing.T) { } func TestGetEnabledCurrencies(t *testing.T) { - enabledPairs := []string{"BTCUSD", "BTCAUD", "LTCUSD", "LTCAUD"} - GetEnabledCurrencies := Base{ - Name: "TESTNAME", - EnabledPairs: enabledPairs, + b := Base{ + Name: "TESTNAME", } - enCurr := GetEnabledCurrencies.GetEnabledCurrencies() - if enCurr[0] != "BTCUSD" { - t.Error("Test Failed - Exchange GetEnabledCurrencies() incorrect string") + b.EnabledPairs = []string{"BTC-USD"} + format := config.CurrencyPairFormatConfig{ + Delimiter: "-", + Index: "", + } + + b.RequestCurrencyPairFormat = format + b.ConfigCurrencyPairFormat = format + c := b.GetEnabledCurrencies() + if c[0].Pair().String() != "BTC-USD" { + t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + } + + format.Delimiter = "~" + b.RequestCurrencyPairFormat = format + c = b.GetEnabledCurrencies() + if c[0].Pair().String() != "BTC-USD" { + t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + } + + format.Delimiter = "" + b.ConfigCurrencyPairFormat = format + c = b.GetEnabledCurrencies() + if c[0].Pair().String() != "BTC-USD" { + t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + } + + b.EnabledPairs = []string{"BTCDOGE"} + format.Index = "BTC" + b.ConfigCurrencyPairFormat = format + c = b.GetEnabledCurrencies() + if c[0].FirstCurrency.String() != "BTC" && c[0].SecondCurrency.String() != "DOGE" { + t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + } + + b.EnabledPairs = []string{"BTC_USD"} + b.RequestCurrencyPairFormat.Delimiter = "" + b.ConfigCurrencyPairFormat.Delimiter = "_" + c = b.GetEnabledCurrencies() + if c[0].FirstCurrency.String() != "BTC" && c[0].SecondCurrency.String() != "USD" { + t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + } + + b.EnabledPairs = []string{"BTCDOGE"} + b.RequestCurrencyPairFormat.Delimiter = "" + b.ConfigCurrencyPairFormat.Delimiter = "" + b.ConfigCurrencyPairFormat.Index = "BTC" + c = b.GetEnabledCurrencies() + if c[0].FirstCurrency.String() != "BTC" && c[0].SecondCurrency.String() != "DOGE" { + t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + } + + b.EnabledPairs = []string{"BTCUSD"} + b.ConfigCurrencyPairFormat.Index = "" + c = b.GetEnabledCurrencies() + if c[0].FirstCurrency.String() != "BTC" && c[0].SecondCurrency.String() != "USD" { + t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") } } func TestGetAvailableCurrencies(t *testing.T) { - availablePairs := []string{"BTCUSD", "BTCAUD", "LTCUSD", "LTCAUD"} - GetEnabledCurrencies := Base{ - Name: "TESTNAME", - AvailablePairs: availablePairs, + b := Base{ + Name: "TESTNAME", } - enCurr := GetEnabledCurrencies.GetAvailableCurrencies() - if enCurr[0] != "BTCUSD" { + b.AvailablePairs = []string{"BTC-USD"} + format := config.CurrencyPairFormatConfig{ + Delimiter: "-", + Index: "", + } + + b.RequestCurrencyPairFormat = format + b.ConfigCurrencyPairFormat = format + c := b.GetAvailableCurrencies() + if c[0].Pair().String() != "BTC-USD" { t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") } + + format.Delimiter = "~" + b.RequestCurrencyPairFormat = format + c = b.GetAvailableCurrencies() + if c[0].Pair().String() != "BTC-USD" { + t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + } + + format.Delimiter = "" + b.ConfigCurrencyPairFormat = format + c = b.GetAvailableCurrencies() + if c[0].Pair().String() != "BTC-USD" { + t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + } + + b.AvailablePairs = []string{"BTCDOGE"} + format.Index = "BTC" + b.ConfigCurrencyPairFormat = format + c = b.GetAvailableCurrencies() + if c[0].FirstCurrency.String() != "BTC" && c[0].SecondCurrency.String() != "DOGE" { + t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + } + + b.AvailablePairs = []string{"BTC_USD"} + b.RequestCurrencyPairFormat.Delimiter = "" + b.ConfigCurrencyPairFormat.Delimiter = "_" + c = b.GetAvailableCurrencies() + if c[0].FirstCurrency.String() != "BTC" && c[0].SecondCurrency.String() != "USD" { + t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + } + + b.AvailablePairs = []string{"BTCDOGE"} + b.RequestCurrencyPairFormat.Delimiter = "" + b.ConfigCurrencyPairFormat.Delimiter = "" + b.ConfigCurrencyPairFormat.Index = "BTC" + c = b.GetAvailableCurrencies() + if c[0].FirstCurrency.String() != "BTC" && c[0].SecondCurrency.String() != "DOGE" { + t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + } + + b.AvailablePairs = []string{"BTCUSD"} + b.ConfigCurrencyPairFormat.Index = "" + c = b.GetAvailableCurrencies() + if c[0].FirstCurrency.String() != "BTC" && c[0].SecondCurrency.String() != "USD" { + t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + } +} + +func TestGetExchangeFormatCurrencySeperator(t *testing.T) { + cfg := config.GetConfig() + err := cfg.LoadConfig(config.ConfigTestFile) + if err != nil { + t.Fatalf("Failed to load config file. Error: %s", err) + } + + expected := true + actual := GetExchangeFormatCurrencySeperator("BTCE") + + if expected != actual { + t.Errorf("Test failed - TestGetExchangeFormatCurrencySeperator expected %v != actual %v", + expected, actual) + } + + expected = false + actual = GetExchangeFormatCurrencySeperator("LocalBitcoins") + + if expected != actual { + t.Errorf("Test failed - TestGetExchangeFormatCurrencySeperator expected %v != actual %v", + expected, actual) + } + + expected = false + actual = GetExchangeFormatCurrencySeperator("blah") + + if expected != actual { + t.Errorf("Test failed - TestGetExchangeFormatCurrencySeperator expected %v != actual %v", + expected, actual) + } +} + +func TestGetAndFormatExchangeCurrencies(t *testing.T) { + cfg := config.GetConfig() + err := cfg.LoadConfig(config.ConfigTestFile) + if err != nil { + t.Fatalf("Failed to load config file. Error: %s", err) + } + + var pairs []pair.CurrencyPair + pairs = append(pairs, pair.NewCurrencyPairDelimiter("BTC_USD", "_")) + pairs = append(pairs, pair.NewCurrencyPairDelimiter("LTC_BTC", "_")) + + actual, err := GetAndFormatExchangeCurrencies("Liqui", pairs) + if err != nil { + t.Errorf("Test failed - Exchange TestGetAndFormatExchangeCurrencies error %s", err) + } + expected := pair.CurrencyItem("btc_usd-ltc_btc") + + if actual.String() != expected.String() { + t.Errorf("Test failed - Exchange TestGetAndFormatExchangeCurrencies %s != %s", + actual, expected) + } + + _, err = GetAndFormatExchangeCurrencies("non-existant", pairs) + if err == nil { + t.Errorf("Test failed - Exchange TestGetAndFormatExchangeCurrencies returned nil error on non-existant exchange") + } +} + +func TestFormatExchangeCurrency(t *testing.T) { + cfg := config.GetConfig() + err := cfg.LoadConfig(config.ConfigTestFile) + if err != nil { + t.Fatalf("Failed to load config file. Error: %s", err) + } + + pair := pair.NewCurrencyPair("BTC", "USD") + expected := "BTC-USD" + actual := FormatExchangeCurrency("GDAX", pair) + + if actual.String() != expected { + t.Errorf("Test failed - Exchange TestFormatExchangeCurrency %s != %s", + actual, expected) + } } func TestFormatCurrency(t *testing.T) { @@ -86,10 +413,17 @@ func TestIsEnabled(t *testing.T) { func TestSetAPIKeys(t *testing.T) { SetAPIKeys := Base{ - Name: "TESTNAME", - Enabled: false, + Name: "TESTNAME", + Enabled: false, + AuthenticatedAPISupport: false, } + SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007", false) + if SetAPIKeys.APIKey != "" && SetAPIKeys.APISecret != "" && SetAPIKeys.ClientID != "" { + t.Error("Test Failed - SetAPIKeys() set values without authenticated API support enabled") + } + + SetAPIKeys.AuthenticatedAPISupport = true SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007", false) if SetAPIKeys.APIKey != "RocketMan" && SetAPIKeys.APISecret != "Digereedoo" && SetAPIKeys.ClientID != "007" { t.Error("Test Failed - Exchange SetAPIKeys() did not set correct values") @@ -97,19 +431,80 @@ func TestSetAPIKeys(t *testing.T) { SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007", true) } -func TestUpdateAvailableCurrencies(t *testing.T) { +func TestUpdateEnabledCurrencies(t *testing.T) { cfg := config.GetConfig() err := cfg.LoadConfig(config.ConfigTestFile) + if err != nil { + t.Fatal("Test failed. TestUpdateEnabledCurrencies failed to load config") + } + UAC := Base{Name: "ANX"} exchangeProducts := []string{"ltc", "btc", "usd", "aud"} - if err != nil { - t.Error( - "Test Failed - Exchange UpdateAvailableCurrencies() did not set correct values", - ) + // Test updating exchange products for an exchange which doesn't exist + UAC.Name = "Blah" + err = UAC.UpdateEnabledCurrencies(exchangeProducts, false) + if err == nil { + t.Errorf("Test Failed - Exchange TestUpdateEnabledCurrencies succeeded on an exchange which doesn't exist") } - err2 := UAC.UpdateAvailableCurrencies(exchangeProducts) - if err2 != nil { - t.Errorf("Test Failed - Exchange UpdateAvailableCurrencies() error: %s", err2) + + // Test updating exchange products + UAC.Name = "ANX" + err = UAC.UpdateEnabledCurrencies(exchangeProducts, false) + if err != nil { + t.Errorf("Test Failed - Exchange TestUpdateEnabledCurrencies error: %s", err) + } + + // Test updating the same new products, diff should be 0 + UAC.Name = "ANX" + err = UAC.UpdateEnabledCurrencies(exchangeProducts, false) + if err != nil { + t.Errorf("Test Failed - Exchange TestUpdateEnabledCurrencies error: %s", err) + } + + // Test force updating to only one product + exchangeProducts = []string{"btc"} + err = UAC.UpdateEnabledCurrencies(exchangeProducts, true) + if err != nil { + t.Errorf("Test Failed - Forced Exchange TestUpdateEnabledCurrencies error: %s", err) + } +} + +func TestUpdateAvailableCurrencies(t *testing.T) { + cfg := config.GetConfig() + err := cfg.LoadConfig(config.ConfigTestFile) + if err != nil { + t.Fatal("Test failed. TestUpdateAvailableCurrencies failed to load config") + } + + UAC := Base{Name: "ANX"} + exchangeProducts := []string{"ltc", "btc", "usd", "aud"} + + // Test updating exchange products for an exchange which doesn't exist + UAC.Name = "Blah" + err = UAC.UpdateAvailableCurrencies(exchangeProducts, false) + if err == nil { + t.Errorf("Test Failed - Exchange UpdateAvailableCurrencies() succeeded on an exchange which doesn't exist") + } + + // Test updating exchange products + UAC.Name = "ANX" + err = UAC.UpdateAvailableCurrencies(exchangeProducts, false) + if err != nil { + t.Errorf("Test Failed - Exchange UpdateAvailableCurrencies() error: %s", err) + } + + // Test updating the same new products, diff should be 0 + UAC.Name = "ANX" + err = UAC.UpdateAvailableCurrencies(exchangeProducts, false) + if err != nil { + t.Errorf("Test Failed - Exchange UpdateAvailableCurrencies() error: %s", err) + } + + // Test force updating to only one product + exchangeProducts = []string{"btc"} + err = UAC.UpdateAvailableCurrencies(exchangeProducts, true) + if err != nil { + t.Errorf("Test Failed - Forced Exchange UpdateAvailableCurrencies() error: %s", err) } } diff --git a/exchanges/gdax/gdax.go b/exchanges/gdax/gdax.go index 7c73f68d..0749ade9 100644 --- a/exchanges/gdax/gdax.go +++ b/exchanges/gdax/gdax.go @@ -7,36 +7,54 @@ import ( "log" "net/url" "strconv" - "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( - GDAX_API_URL = "https://api.gdax.com/" - GDAX_API_VERSION = "0" - GDAX_PRODUCTS = "products" - GDAX_ORDERBOOK = "book" - GDAX_TICKER = "ticker" - GDAX_TRADES = "trades" - GDAX_HISTORY = "candles" - GDAX_STATS = "stats" - GDAX_CURRENCIES = "currencies" - GDAX_ACCOUNTS = "accounts" - GDAX_LEDGER = "ledger" - GDAX_HOLDS = "holds" - GDAX_ORDERS = "orders" - GDAX_FILLS = "fills" - GDAX_TRANSFERS = "transfers" - GDAX_REPORTS = "reports" + gdaxAPIURL = "https://api.gdax.com/" + gdaxAPIVersion = "0" + gdaxProducts = "products" + gdaxOrderbook = "book" + gdaxTicker = "ticker" + gdaxTrades = "trades" + gdaxHistory = "candles" + gdaxStats = "stats" + gdaxCurrencies = "currencies" + gdaxAccounts = "accounts" + gdaxLedger = "ledger" + gdaxHolds = "holds" + gdaxOrders = "orders" + gdaxFills = "fills" + gdaxTransfers = "transfers" + gdaxReports = "reports" + gdaxTime = "time" + gdaxMarginTransfer = "profiles/margin-transfer" + gdaxFunding = "funding" + gdaxFundingRepay = "funding/repay" + gdaxPosition = "position" + gdaxPositionClose = "position/close" + gdaxPaymentMethod = "payment-methods" + gdaxPaymentMethodDeposit = "deposits/payment-method" + gdaxDepositCoinbase = "deposits/coinbase-account" + gdaxWithdrawalPaymentMethod = "withdrawals/payment-method" + gdaxWithdrawalCoinbase = "withdrawals/coinbase" + gdaxWithdrawalCrypto = "withdrawals/crypto" + gdaxCoinbaseAccounts = "coinbase-accounts" + gdaxTrailingVolume = "users/self/trailing-volume" ) +var sometin []string + +// GDAX is the overarching type across the GDAX package type GDAX struct { exchange.Base } +// SetDefaults sets default values for the exchange func (g *GDAX) SetDefaults() { g.Name = "GDAX" g.Enabled = false @@ -46,8 +64,14 @@ func (g *GDAX) SetDefaults() { g.Verbose = false g.Websocket = false g.RESTPollingDelay = 10 + g.RequestCurrencyPairFormat.Delimiter = "-" + g.RequestCurrencyPairFormat.Uppercase = true + g.ConfigCurrencyPairFormat.Delimiter = "" + g.ConfigCurrencyPairFormat.Uppercase = true + g.AssetTypes = []string{ticker.Spot} } +// Setup initialises the exchange paramaters with the current configuration func (g *GDAX) Setup(exch config.ExchangeConfig) { if !exch.Enabled { g.SetEnabled(false) @@ -61,45 +85,50 @@ func (g *GDAX) Setup(exch config.ExchangeConfig) { g.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") g.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") g.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := g.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = g.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } +// GetFee returns the current fee for the exchange func (g *GDAX) GetFee(maker bool) float64 { if maker { return g.MakerFee - } else { - return g.TakerFee } + return g.TakerFee } -func (g *GDAX) GetProducts() ([]GDAXProduct, error) { - products := []GDAXProduct{} - err := common.SendHTTPGetRequest(GDAX_API_URL+GDAX_PRODUCTS, true, &products) +// GetProducts returns supported currency pairs on the exchange with specific +// information about the pair +func (g *GDAX) GetProducts() ([]Product, error) { + products := []Product{} - if err != nil { - return nil, err - } - - return products, nil + return products, + common.SendHTTPGetRequest(gdaxAPIURL+gdaxProducts, true, g.Verbose, &products) } +// GetOrderbook returns orderbook by currency pair and level func (g *GDAX) GetOrderbook(symbol string, level int) (interface{}, error) { - orderbook := GDAXOrderbookResponse{} - path := "" + orderbook := OrderbookResponse{} + + path := fmt.Sprintf("%s/%s/%s", gdaxAPIURL+gdaxProducts, symbol, gdaxOrderbook) if level > 0 { levelStr := strconv.Itoa(level) - path = fmt.Sprintf("%s/%s/%s?level=%s", GDAX_API_URL+GDAX_PRODUCTS, symbol, GDAX_ORDERBOOK, levelStr) - } else { - path = fmt.Sprintf("%s/%s/%s", GDAX_API_URL+GDAX_PRODUCTS, symbol, GDAX_ORDERBOOK) + path = fmt.Sprintf("%s/%s/%s?level=%s", gdaxAPIURL+gdaxProducts, symbol, gdaxOrderbook, levelStr) } - err := common.SendHTTPGetRequest(path, true, &orderbook) - if err != nil { + if err := common.SendHTTPGetRequest(path, true, g.Verbose, &orderbook); err != nil { return nil, err } if level == 3 { - ob := GDAXOrderbookL3{} + ob := OrderbookL3{} ob.Sequence = orderbook.Sequence for _, x := range orderbook.Asks { price, err := strconv.ParseFloat((x[0].(string)), 64) @@ -111,7 +140,7 @@ func (g *GDAX) GetOrderbook(symbol string, level int) (interface{}, error) { continue } - ob.Asks = append(ob.Asks, GDAXOrderL3{Price: price, Amount: amount, OrderID: x[2].(string)}) + ob.Asks = append(ob.Asks, OrderL3{Price: price, Amount: amount, OrderID: x[2].(string)}) } for _, x := range orderbook.Bids { price, err := strconv.ParseFloat((x[0].(string)), 64) @@ -123,64 +152,65 @@ func (g *GDAX) GetOrderbook(symbol string, level int) (interface{}, error) { continue } - ob.Bids = append(ob.Bids, GDAXOrderL3{Price: price, Amount: amount, OrderID: x[2].(string)}) - } - return ob, nil - } else { - ob := GDAXOrderbookL1L2{} - ob.Sequence = orderbook.Sequence - for _, x := range orderbook.Asks { - price, err := strconv.ParseFloat((x[0].(string)), 64) - if err != nil { - continue - } - amount, err := strconv.ParseFloat((x[1].(string)), 64) - if err != nil { - continue - } - - ob.Asks = append(ob.Asks, GDAXOrderL1L2{Price: price, Amount: amount, NumOrders: x[2].(float64)}) - } - for _, x := range orderbook.Bids { - price, err := strconv.ParseFloat((x[0].(string)), 64) - if err != nil { - continue - } - amount, err := strconv.ParseFloat((x[1].(string)), 64) - if err != nil { - continue - } - - ob.Bids = append(ob.Bids, GDAXOrderL1L2{Price: price, Amount: amount, NumOrders: x[2].(float64)}) + ob.Bids = append(ob.Bids, OrderL3{Price: price, Amount: amount, OrderID: x[2].(string)}) } return ob, nil } -} + ob := OrderbookL1L2{} + ob.Sequence = orderbook.Sequence + for _, x := range orderbook.Asks { + price, err := strconv.ParseFloat((x[0].(string)), 64) + if err != nil { + continue + } + amount, err := strconv.ParseFloat((x[1].(string)), 64) + if err != nil { + continue + } -func (g *GDAX) GetTicker(symbol string) (GDAXTicker, error) { - ticker := GDAXTicker{} - path := fmt.Sprintf("%s/%s/%s", GDAX_API_URL+GDAX_PRODUCTS, symbol, GDAX_TICKER) - err := common.SendHTTPGetRequest(path, true, &ticker) - - if err != nil { - return ticker, err + ob.Asks = append(ob.Asks, OrderL1L2{Price: price, Amount: amount, NumOrders: x[2].(float64)}) } - return ticker, nil -} + for _, x := range orderbook.Bids { + price, err := strconv.ParseFloat((x[0].(string)), 64) + if err != nil { + continue + } + amount, err := strconv.ParseFloat((x[1].(string)), 64) + if err != nil { + continue + } -func (g *GDAX) GetTrades(symbol string) ([]GDAXTrade, error) { - trades := []GDAXTrade{} - path := fmt.Sprintf("%s/%s/%s", GDAX_API_URL+GDAX_PRODUCTS, symbol, GDAX_TRADES) - err := common.SendHTTPGetRequest(path, true, &trades) - - if err != nil { - return nil, err + ob.Bids = append(ob.Bids, OrderL1L2{Price: price, Amount: amount, NumOrders: x[2].(float64)}) } - return trades, nil + return ob, nil } -func (g *GDAX) GetHistoricRates(symbol string, start, end, granularity int64) ([]GDAXHistory, error) { - history := []GDAXHistory{} +// GetTicker returns ticker by currency pair +// currencyPair - example "BTC-USD" +func (g *GDAX) GetTicker(currencyPair string) (Ticker, error) { + ticker := Ticker{} + path := fmt.Sprintf( + "%s/%s/%s", gdaxAPIURL+gdaxProducts, currencyPair, gdaxTicker) + + log.Println(path) + return ticker, common.SendHTTPGetRequest(path, true, g.Verbose, &ticker) +} + +// GetTrades listd the latest trades for a product +// currencyPair - example "BTC-USD" +func (g *GDAX) GetTrades(currencyPair string) ([]Trade, error) { + trades := []Trade{} + path := fmt.Sprintf( + "%s/%s/%s", gdaxAPIURL+gdaxProducts, currencyPair, gdaxTrades) + + return trades, common.SendHTTPGetRequest(path, true, g.Verbose, &trades) +} + +// GetHistoricRates returns historic rates for a product. Rates are returned in +// grouped buckets based on requested granularity. +func (g *GDAX) GetHistoricRates(currencyPair string, start, end, granularity int64) ([]History, error) { + var resp [][]interface{} + history := []History{} values := url.Values{} if start > 0 { @@ -195,97 +225,137 @@ func (g *GDAX) GetHistoricRates(symbol string, start, end, granularity int64) ([ values.Set("granularity", strconv.FormatInt(granularity, 10)) } - path := common.EncodeURLValues(fmt.Sprintf("%s/%s/%s", GDAX_API_URL+GDAX_PRODUCTS, symbol, GDAX_HISTORY), values) - err := common.SendHTTPGetRequest(path, true, &history) + path := common.EncodeURLValues( + fmt.Sprintf("%s/%s/%s", gdaxAPIURL+gdaxProducts, currencyPair, gdaxHistory), + values) - if err != nil { - return nil, err + if err := common.SendHTTPGetRequest(path, true, g.Verbose, &resp); err != nil { + return history, err } + + for _, single := range resp { + s := History{ + Time: int64(single[0].(float64)), + Low: single[1].(float64), + High: single[2].(float64), + Open: single[3].(float64), + Close: single[4].(float64), + Volume: single[5].(float64), + } + history = append(history, s) + } + return history, nil } -func (g *GDAX) GetStats(symbol string) (GDAXStats, error) { - stats := GDAXStats{} - path := fmt.Sprintf("%s/%s/%s", GDAX_API_URL+GDAX_PRODUCTS, symbol, GDAX_STATS) - err := common.SendHTTPGetRequest(path, true, &stats) +// GetStats returns a 24 hr stat for the product. Volume is in base currency +// units. open, high, low are in quote currency units. +func (g *GDAX) GetStats(currencyPair string) (Stats, error) { + stats := Stats{} + path := fmt.Sprintf( + "%s/%s/%s", gdaxAPIURL+gdaxProducts, currencyPair, gdaxStats) - if err != nil { - return stats, err - } - return stats, nil + return stats, common.SendHTTPGetRequest(path, true, g.Verbose, &stats) } -func (g *GDAX) GetCurrencies() ([]GDAXCurrency, error) { - currencies := []GDAXCurrency{} - err := common.SendHTTPGetRequest(GDAX_API_URL+GDAX_CURRENCIES, true, ¤cies) +// GetCurrencies returns a list of supported currency on the exchange +// Warning: Not all currencies may be currently in use for trading. +func (g *GDAX) GetCurrencies() ([]Currency, error) { + currencies := []Currency{} - if err != nil { - return nil, err - } - return currencies, nil + return currencies, + common.SendHTTPGetRequest(gdaxAPIURL+gdaxCurrencies, true, g.Verbose, ¤cies) } -func (g *GDAX) GetAccounts() ([]GDAXAccountResponse, error) { - resp := []GDAXAccountResponse{} - err := g.SendAuthenticatedHTTPRequest("GET", GDAX_ACCOUNTS, nil, &resp) - if err != nil { - return nil, err - } - return resp, nil +// GetServerTime returns the API server time +func (g *GDAX) GetServerTime() (ServerTime, error) { + serverTime := ServerTime{} + + return serverTime, + common.SendHTTPGetRequest(gdaxAPIURL+gdaxTime, true, g.Verbose, &serverTime) } -func (g *GDAX) GetAccount(account string) (GDAXAccountResponse, error) { - resp := GDAXAccountResponse{} - path := fmt.Sprintf("%s/%s", GDAX_ACCOUNTS, account) - err := g.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) - if err != nil { - return resp, err - } - return resp, nil +// GetAccounts returns a list of trading accounts associated with the APIKEYS +func (g *GDAX) GetAccounts() ([]AccountResponse, error) { + resp := []AccountResponse{} + + return resp, + g.SendAuthenticatedHTTPRequest("GET", gdaxAccounts, nil, &resp) } -func (g *GDAX) GetAccountHistory(accountID string) ([]GDAXAccountLedgerResponse, error) { - resp := []GDAXAccountLedgerResponse{} - path := fmt.Sprintf("%s/%s/%s", GDAX_ACCOUNTS, accountID, GDAX_LEDGER) - err := g.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) - if err != nil { - return nil, err - } - return resp, nil +// GetAccount returns information for a single account. Use this endpoint when +// account_id is known +func (g *GDAX) GetAccount(accountID string) (AccountResponse, error) { + resp := AccountResponse{} + path := fmt.Sprintf("%s/%s", gdaxAccounts, accountID) + + return resp, g.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) } -func (g *GDAX) GetHolds(accountID string) ([]GDAXAccountHolds, error) { - resp := []GDAXAccountHolds{} - path := fmt.Sprintf("%s/%s/%s", GDAX_ACCOUNTS, accountID, GDAX_HOLDS) - err := g.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) - if err != nil { - return nil, err - } - return resp, nil +// GetAccountHistory returns a list of account activity. Account activity either +// increases or decreases your account balance. Items are paginated and sorted +// latest first. +func (g *GDAX) GetAccountHistory(accountID string) ([]AccountLedgerResponse, error) { + resp := []AccountLedgerResponse{} + path := fmt.Sprintf("%s/%s/%s", gdaxAccounts, accountID, gdaxLedger) + + return resp, g.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) } -func (g *GDAX) PlaceOrder(clientRef string, price, amount float64, side string, productID, stp string) (string, error) { +// GetHolds returns the holds that are placed on an account for any active +// orders or pending withdraw requests. As an order is filled, the hold amount +// is updated. If an order is canceled, any remaining hold is removed. For a +// withdraw, once it is completed, the hold is removed. +func (g *GDAX) GetHolds(accountID string) ([]AccountHolds, error) { + resp := []AccountHolds{} + path := fmt.Sprintf("%s/%s/%s", gdaxAccounts, accountID, gdaxHolds) + + return resp, g.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) +} + +// PlaceLimitOrder places a new limit order. Orders can only be placed if the +// account has sufficient funds. Once an order is placed, account funds +// will be put on hold for the duration of the order. How much and which funds +// are put on hold depends on the order type and parameters specified. +// +// GENERAL PARAMS +// clientRef - [optional] Order ID selected by you to identify your order +// side - buy or sell +// productID - A valid product id +// stp - [optional] Self-trade prevention flag +// +// LIMIT ORDER PARAMS +// price - Price per bitcoin +// amount - Amount of BTC to buy or sell +// timeInforce - [optional] GTC, GTT, IOC, or FOK (default is GTC) +// cancelAfter - [optional] min, hour, day * Requires time_in_force to be GTT +// postOnly - [optional] Post only flag Invalid when time_in_force is IOC or FOK +func (g *GDAX) PlaceLimitOrder(clientRef string, price, amount float64, side, timeInforce, cancelAfter, productID, stp string, postOnly bool) (string, error) { + resp := GeneralizedOrderResponse{} request := make(map[string]interface{}) - - if clientRef != "" { - request["client_oid"] = clientRef - } - + request["type"] = "limit" request["price"] = strconv.FormatFloat(price, 'f', -1, 64) request["size"] = strconv.FormatFloat(amount, 'f', -1, 64) request["side"] = side request["product_id"] = productID + if cancelAfter != "" { + request["cancel_after"] = cancelAfter + } + if timeInforce != "" { + request["time_in_foce"] = timeInforce + } + if clientRef != "" { + request["client_oid"] = clientRef + } if stp != "" { request["stp"] = stp } - - type OrderResponse struct { - ID string `json:"id"` + if postOnly { + request["post_only"] = postOnly } - resp := OrderResponse{} - err := g.SendAuthenticatedHTTPRequest("POST", GDAX_ORDERS, request, &resp) + err := g.SendAuthenticatedHTTPRequest("POST", gdaxOrders, request, &resp) if err != nil { return "", err } @@ -293,98 +363,406 @@ func (g *GDAX) PlaceOrder(clientRef string, price, amount float64, side string, return resp.ID, nil } +// PlaceMarketOrder places a new market order. +// Orders can only be placed if the account has sufficient funds. Once an order +// is placed, account funds will be put on hold for the duration of the order. +// How much and which funds are put on hold depends on the order type and +// parameters specified. +// +// GENERAL PARAMS +// clientRef - [optional] Order ID selected by you to identify your order +// side - buy or sell +// productID - A valid product id +// stp - [optional] Self-trade prevention flag +// +// MARKET ORDER PARAMS +// size - [optional]* Desired amount in BTC +// funds [optional]* Desired amount of quote currency to use +// * One of size or funds is required. +func (g *GDAX) PlaceMarketOrder(clientRef string, size, funds float64, side string, productID, stp string) (string, error) { + resp := GeneralizedOrderResponse{} + request := make(map[string]interface{}) + request["side"] = side + request["product_id"] = productID + request["type"] = "market" + + if size != 0 { + request["size"] = strconv.FormatFloat(size, 'f', -1, 64) + } + if funds != 0 { + request["funds"] = strconv.FormatFloat(funds, 'f', -1, 64) + } + if clientRef != "" { + request["client_oid"] = clientRef + } + if stp != "" { + request["stp"] = stp + } + + err := g.SendAuthenticatedHTTPRequest("POST", gdaxOrders, request, &resp) + if err != nil { + return "", err + } + + return resp.ID, nil +} + +// PlaceMarginOrder places a new market order. +// Orders can only be placed if the account has sufficient funds. Once an order +// is placed, account funds will be put on hold for the duration of the order. +// How much and which funds are put on hold depends on the order type and +// parameters specified. +// +// GENERAL PARAMS +// clientRef - [optional] Order ID selected by you to identify your order +// side - buy or sell +// productID - A valid product id +// stp - [optional] Self-trade prevention flag +// +// MARGIN ORDER PARAMS +// size - [optional]* Desired amount in BTC +// funds - [optional]* Desired amount of quote currency to use +func (g *GDAX) PlaceMarginOrder(clientRef string, size, funds float64, side string, productID, stp string) (string, error) { + resp := GeneralizedOrderResponse{} + request := make(map[string]interface{}) + request["side"] = side + request["product_id"] = productID + request["type"] = "margin" + + if size != 0 { + request["size"] = strconv.FormatFloat(size, 'f', -1, 64) + } + if funds != 0 { + request["funds"] = strconv.FormatFloat(funds, 'f', -1, 64) + } + if clientRef != "" { + request["client_oid"] = clientRef + } + if stp != "" { + request["stp"] = stp + } + + err := g.SendAuthenticatedHTTPRequest("POST", gdaxOrders, request, &resp) + if err != nil { + return "", err + } + + return resp.ID, nil +} + +// CancelOrder cancels order by orderID func (g *GDAX) CancelOrder(orderID string) error { - path := fmt.Sprintf("%s/%s", GDAX_ORDERS, orderID) - err := g.SendAuthenticatedHTTPRequest("DELETE", path, nil, nil) - if err != nil { - return err - } - return nil + path := fmt.Sprintf("%s/%s", gdaxOrders, orderID) + + return g.SendAuthenticatedHTTPRequest("DELETE", path, nil, nil) } -func (g *GDAX) GetOrders(params url.Values) ([]GDAXOrdersResponse, error) { - path := common.EncodeURLValues(GDAX_API_URL+GDAX_ORDERS, params) - resp := []GDAXOrdersResponse{} - err := g.SendAuthenticatedHTTPRequest("GET", common.GetURIPath(path), nil, &resp) - if err != nil { - return nil, err +// CancelAllOrders cancels all open orders on the exchange and returns and array +// of order IDs +// currencyPair - [optional] all orders for a currencyPair string will be +// canceled +func (g *GDAX) CancelAllOrders(currencyPair string) ([]string, error) { + var resp []string + request := make(map[string]interface{}) + + if len(currencyPair) != 0 { + request["product_id"] = currencyPair } - return resp, nil + return resp, g.SendAuthenticatedHTTPRequest("DELETE", gdaxOrders, request, &resp) } -func (g *GDAX) GetOrder(orderID string) (GDAXOrderResponse, error) { - path := fmt.Sprintf("%s/%s", GDAX_ORDERS, orderID) - resp := GDAXOrderResponse{} - err := g.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) - if err != nil { - return resp, err +// GetOrders lists current open orders. Only open or un-settled orders are +// returned. As soon as an order is no longer open and settled, it will no +// longer appear in the default request. +// status - can be a range of "open", "pending", "done" or "active" +// currencyPair - [optional] for example "BTC-USD" +func (g *GDAX) GetOrders(status []string, currencyPair string) ([]GeneralizedOrderResponse, error) { + resp := []GeneralizedOrderResponse{} + params := url.Values{} + + for _, individualStatus := range status { + params.Add("status", individualStatus) } - return resp, nil + if len(currencyPair) != 0 { + params.Set("product_id", currencyPair) + } + + path := common.EncodeURLValues(gdaxAPIURL+gdaxOrders, params) + path = common.GetURIPath(path) + + return resp, + g.SendAuthenticatedHTTPRequest("GET", path[1:], nil, &resp) } -func (g *GDAX) GetFills(params url.Values) ([]GDAXFillResponse, error) { - path := common.EncodeURLValues(GDAX_API_URL+GDAX_FILLS, params) - resp := []GDAXFillResponse{} - err := g.SendAuthenticatedHTTPRequest("GET", common.GetURIPath(path), nil, &resp) - if err != nil { - return nil, err - } - return resp, nil +// GetOrder returns a single order by order id. +func (g *GDAX) GetOrder(orderID string) (GeneralizedOrderResponse, error) { + resp := GeneralizedOrderResponse{} + path := fmt.Sprintf("%s/%s", gdaxOrders, orderID) + + return resp, g.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) } -func (g *GDAX) Transfer(transferType string, amount float64, accountID string) error { +// GetFills returns a list of recent fills +func (g *GDAX) GetFills(orderID, currencyPair string) ([]FillResponse, error) { + resp := []FillResponse{} + params := url.Values{} + + if len(orderID) != 0 { + params.Set("order_id", orderID) + } + if len(currencyPair) != 0 { + params.Set("product_id", currencyPair) + } + if len(params.Get("order_id")) == 0 && len(params.Get("product_id")) == 0 { + return resp, errors.New("no paramaters set") + } + + path := common.EncodeURLValues(gdaxAPIURL+gdaxFills, params) + uri := common.GetURIPath(path) + + return resp, + g.SendAuthenticatedHTTPRequest("GET", uri[1:], nil, &resp) +} + +// GetFundingRecords every order placed with a margin profile that draws funding +// will create a funding record. +// +// status - "outstanding", "settled", or "rejected" +func (g *GDAX) GetFundingRecords(status string) ([]Funding, error) { + resp := []Funding{} + params := url.Values{} + params.Set("status", status) + + path := common.EncodeURLValues(gdaxAPIURL+gdaxFunding, params) + uri := common.GetURIPath(path) + + return resp, + g.SendAuthenticatedHTTPRequest("GET", uri[1:], nil, &resp) +} + +////////////////////////// Not receiving reply from server ///////////////// +// RepayFunding repays the older funding records first +// +// amount - amount of currency to repay +// currency - currency, example USD +// func (g *GDAX) RepayFunding(amount, currency string) (Funding, error) { +// resp := Funding{} +// params := make(map[string]interface{}) +// params["amount"] = amount +// params["currency"] = currency +// +// return resp, +// g.SendAuthenticatedHTTPRequest("POST", gdaxFundingRepay, params, &resp) +// } + +// MarginTransfer sends funds between a standard/default profile and a margin +// profile. +// A deposit will transfer funds from the default profile into the margin +// profile. A withdraw will transfer funds from the margin profile to the +// default profile. Withdraws will fail if they would set your margin ratio +// below the initial margin ratio requirement. +// +// amount - the amount to transfer between the default and margin profile +// transferType - either "deposit" or "withdraw" +// profileID - The id of the margin profile to deposit or withdraw from +// currency - currency to transfer, currently on "BTC" or "USD" +func (g *GDAX) MarginTransfer(amount float64, transferType, profileID, currency string) (MarginTransfer, error) { + resp := MarginTransfer{} request := make(map[string]interface{}) request["type"] = transferType request["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) - request["GDAX_account_id"] = accountID + request["currency"] = currency + request["margin_profile_id"] = profileID - err := g.SendAuthenticatedHTTPRequest("POST", GDAX_TRANSFERS, request, nil) - if err != nil { - return err - } - return nil + return resp, + g.SendAuthenticatedHTTPRequest("POST", gdaxMarginTransfer, request, &resp) } -func (g *GDAX) GetReport(reportType, startDate, endDate string) (GDAXReportResponse, error) { +// GetPosition returns an overview of account profile. +func (g *GDAX) GetPosition() (AccountOverview, error) { + resp := AccountOverview{} + + return resp, + g.SendAuthenticatedHTTPRequest("GET", gdaxPosition, nil, &resp) +} + +// ClosePosition closes a position and allowing you to repay position as well +// repayOnly - allows the position to be repaid +func (g *GDAX) ClosePosition(repayOnly bool) (AccountOverview, error) { + resp := AccountOverview{} + request := make(map[string]interface{}) + request["repay_only"] = repayOnly + + return resp, + g.SendAuthenticatedHTTPRequest("POST", gdaxPositionClose, request, &resp) +} + +// GetPayMethods returns a full list of payment methods +func (g *GDAX) GetPayMethods() ([]PaymentMethod, error) { + resp := []PaymentMethod{} + + return resp, + g.SendAuthenticatedHTTPRequest("GET", gdaxPaymentMethod, nil, &resp) +} + +// DepositViaPaymentMethod deposits funds from a payment method. See the Payment +// Methods section for retrieving your payment methods. +// +// amount - The amount to deposit +// currency - The type of currency +// paymentID - ID of the payment method +func (g *GDAX) DepositViaPaymentMethod(amount float64, currency, paymentID string) (DepositWithdrawalInfo, error) { + resp := DepositWithdrawalInfo{} + req := make(map[string]interface{}) + req["amount"] = amount + req["currency"] = currency + req["payment_method_id"] = paymentID + + return resp, + g.SendAuthenticatedHTTPRequest("POST", gdaxPaymentMethodDeposit, req, &resp) +} + +// DepositViaCoinbase deposits funds from a coinbase account. Move funds between +// a Coinbase account and GDAX trading account within daily limits. Moving +// funds between Coinbase and GDAX is instant and free. See the Coinbase +// Accounts section for retrieving your Coinbase accounts. +// +// amount - The amount to deposit +// currency - The type of currency +// accountID - ID of the coinbase account +func (g *GDAX) DepositViaCoinbase(amount float64, currency, accountID string) (DepositWithdrawalInfo, error) { + resp := DepositWithdrawalInfo{} + req := make(map[string]interface{}) + req["amount"] = amount + req["currency"] = currency + req["coinbase_account_id"] = accountID + + return resp, + g.SendAuthenticatedHTTPRequest("POST", gdaxDepositCoinbase, req, &resp) +} + +// WithdrawViaPaymentMethod withdraws funds to a payment method +// +// amount - The amount to withdraw +// currency - The type of currency +// paymentID - ID of the payment method +func (g *GDAX) WithdrawViaPaymentMethod(amount float64, currency, paymentID string) (DepositWithdrawalInfo, error) { + resp := DepositWithdrawalInfo{} + req := make(map[string]interface{}) + req["amount"] = amount + req["currency"] = currency + req["payment_method_id"] = paymentID + + return resp, + g.SendAuthenticatedHTTPRequest("POST", gdaxWithdrawalPaymentMethod, req, &resp) +} + +///////////////////////// NO ROUTE FOUND ERROR //////////////////////////////// +// WithdrawViaCoinbase withdraws funds to a coinbase account. +// +// amount - The amount to withdraw +// currency - The type of currency +// accountID - ID of the coinbase account +// func (g *GDAX) WithdrawViaCoinbase(amount float64, currency, accountID string) (DepositWithdrawalInfo, error) { +// resp := DepositWithdrawalInfo{} +// req := make(map[string]interface{}) +// req["amount"] = amount +// req["currency"] = currency +// req["coinbase_account_id"] = accountID +// +// return resp, +// g.SendAuthenticatedHTTPRequest("POST", gdaxWithdrawalCoinbase, req, &resp) +// } + +// WithdrawCrypto withdraws funds to a crypto address +// +// amount - The amount to withdraw +// currency - The type of currency +// cryptoAddress - A crypto address of the recipient +func (g *GDAX) WithdrawCrypto(amount float64, currency, cryptoAddress string) (DepositWithdrawalInfo, error) { + resp := DepositWithdrawalInfo{} + req := make(map[string]interface{}) + req["amount"] = amount + req["currency"] = currency + req["crypto_address"] = cryptoAddress + + return resp, + g.SendAuthenticatedHTTPRequest("POST", gdaxWithdrawalCrypto, req, &resp) +} + +// GetCoinbaseAccounts returns a list of coinbase accounts +func (g *GDAX) GetCoinbaseAccounts() ([]CoinbaseAccounts, error) { + resp := []CoinbaseAccounts{} + + return resp, + g.SendAuthenticatedHTTPRequest("GET", gdaxCoinbaseAccounts, nil, &resp) +} + +// GetReport returns batches of historic information about your account in +// various human and machine readable forms. +// +// reportType - "fills" or "account" +// startDate - Starting date for the report (inclusive) +// endDate - Ending date for the report (inclusive) +// currencyPair - ID of the product to generate a fills report for. +// E.g. BTC-USD. *Required* if type is fills +// accountID - ID of the account to generate an account report for. *Required* +// if type is account +// format - pdf or csv (defualt is pdf) +// email - [optional] Email address to send the report to +func (g *GDAX) GetReport(reportType, startDate, endDate, currencyPair, accountID, format, email string) (Report, error) { + resp := Report{} request := make(map[string]interface{}) request["type"] = reportType request["start_date"] = startDate request["end_date"] = endDate + request["format"] = "pdf" - resp := GDAXReportResponse{} - err := g.SendAuthenticatedHTTPRequest("POST", GDAX_REPORTS, request, &resp) - if err != nil { - return resp, err + if len(currencyPair) != 0 { + request["product_id"] = currencyPair } - return resp, nil + if len(accountID) != 0 { + request["account_id"] = accountID + } + if format == "csv" { + request["format"] = format + } + if len(email) != 0 { + request["email"] = email + } + + return resp, + g.SendAuthenticatedHTTPRequest("POST", gdaxReports, request, &resp) } -func (g *GDAX) GetReportStatus(reportID string) (GDAXReportResponse, error) { - path := fmt.Sprintf("%s/%s", GDAX_REPORTS, reportID) - resp := GDAXReportResponse{} - err := g.SendAuthenticatedHTTPRequest("POST", path, nil, &resp) - if err != nil { - return resp, err - } - return resp, nil +// GetReportStatus once a report request has been accepted for processing, the +// status is available by polling the report resource endpoint. +func (g *GDAX) GetReportStatus(reportID string) (Report, error) { + resp := Report{} + path := fmt.Sprintf("%s/%s", gdaxReports, reportID) + + return resp, g.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) } +// GetTrailingVolume this request will return your 30-day trailing volume for +// all products. +func (g *GDAX) GetTrailingVolume() ([]Volume, error) { + resp := []Volume{} + + return resp, + g.SendAuthenticatedHTTPRequest("GET", gdaxTrailingVolume, nil, &resp) +} + +// SendAuthenticatedHTTPRequest sends an authenticated HTTP reque func (g *GDAX) SendAuthenticatedHTTPRequest(method, path string, params map[string]interface{}, result interface{}) (err error) { if !g.AuthenticatedAPISupport { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, g.Name) } - if g.Nonce.Get() == 0 { - g.Nonce.Set(time.Now().Unix()) - } else { - g.Nonce.Inc() - } - payload := []byte("") if params != nil { payload, err = common.JSONEncode(params) - if err != nil { return errors.New("SendAuthenticatedHTTPRequest: Unable to JSON request") } @@ -394,26 +772,34 @@ func (g *GDAX) SendAuthenticatedHTTPRequest(method, path string, params map[stri } } - message := g.Nonce.String() + method + "/" + path + string(payload) + nonce := g.Nonce.GetValue(g.Name, false).String() + message := nonce + method + "/" + path + string(payload) hmac := common.GetHMAC(common.HashSHA256, []byte(message), []byte(g.APISecret)) headers := make(map[string]string) headers["CB-ACCESS-SIGN"] = common.Base64Encode([]byte(hmac)) - headers["CB-ACCESS-TIMESTAMP"] = g.Nonce.String() + headers["CB-ACCESS-TIMESTAMP"] = nonce headers["CB-ACCESS-KEY"] = g.APIKey headers["CB-ACCESS-PASSPHRASE"] = g.ClientID headers["Content-Type"] = "application/json" - resp, err := common.SendHTTPRequest(method, GDAX_API_URL+path, headers, bytes.NewBuffer(payload)) + resp, err := common.SendHTTPRequest(method, gdaxAPIURL+path, headers, bytes.NewBuffer(payload)) + if err != nil { + return err + } if g.Verbose { log.Printf("Received raw: \n%s\n", resp) } - err = common.JSONDecode([]byte(resp), &result) + type initialResponse struct { + Message string `json:"message"` + } + initialCheck := initialResponse{} - if err != nil { - return errors.New("unable to JSON Unmarshal response") + err = common.JSONDecode([]byte(resp), &initialCheck) + if err == nil && len(initialCheck.Message) != 0 { + return errors.New(initialCheck.Message) } - return nil + return common.JSONDecode([]byte(resp), &result) } diff --git a/exchanges/gdax/gdax_test.go b/exchanges/gdax/gdax_test.go new file mode 100644 index 00000000..fd2ae99a --- /dev/null +++ b/exchanges/gdax/gdax_test.go @@ -0,0 +1,300 @@ +package gdax + +import ( + "testing" + + "github.com/thrasher-/gocryptotrader/config" +) + +var g GDAX + +// Please supply your APIKeys here for better testing +const ( + apiKey = "" + apiSecret = "" + clientID = "" //passphrase you made at API CREATION +) + +func TestSetDefaults(t *testing.T) { + g.SetDefaults() +} + +func TestSetup(t *testing.T) { + cfg := config.GetConfig() + cfg.LoadConfig("../../testdata/configtest.dat") + gdxConfig, err := cfg.GetExchangeConfig("Bitfinex") + if err != nil { + t.Error("Test Failed - GDAX Setup() init error") + } + + g.Setup(gdxConfig) +} + +func TestGetFee(t *testing.T) { + t.Parallel() + if g.GetFee(false) == 0 { + t.Error("Test failed - GetFee() error") + } + if g.GetFee(true) != 0 { + t.Error("Test failed - GetFee() error") + } +} + +func TestGetProducts(t *testing.T) { + t.Parallel() + _, err := g.GetProducts() + if err != nil { + t.Error("Test failed - GetProducts() error") + } +} + +func TestGetTicker(t *testing.T) { + t.Parallel() + _, err := g.GetTicker("BTC-USD") + if err != nil { + t.Error("Test failed - GetTicker() error", err) + } +} + +func TestGetTrades(t *testing.T) { + t.Parallel() + _, err := g.GetTrades("BTC-USD") + if err != nil { + t.Error("Test failed - GetTrades() error", err) + } +} + +func TestGetHistoricRates(t *testing.T) { + t.Parallel() + _, err := g.GetHistoricRates("BTC-USD", 0, 0, 0) + if err != nil { + t.Error("Test failed - GetHistoricRates() error", err) + } +} + +func TestGetStats(t *testing.T) { + t.Parallel() + _, err := g.GetStats("BTC-USD") + if err != nil { + t.Error("Test failed - GetStats() error", err) + } +} + +func TestGetCurrencies(t *testing.T) { + t.Parallel() + _, err := g.GetCurrencies() + if err != nil { + t.Error("Test failed - GetCurrencies() error", err) + } +} + +func TestGetServerTime(t *testing.T) { + t.Parallel() + _, err := g.GetServerTime() + if err != nil { + t.Error("Test failed - GetServerTime() error", err) + } +} + +func TestGetAccounts(t *testing.T) { + t.Parallel() + _, err := g.GetAccounts() + if err == nil { + t.Error("Test failed - GetAccounts() error", err) + } +} + +func TestGetAccount(t *testing.T) { + t.Parallel() + _, err := g.GetAccount("234cb213-ac6f-4ed8-b7b6-e62512930945") + if err == nil { + t.Error("Test failed - GetAccount() error", err) + } +} + +func TestGetAccountHistory(t *testing.T) { + t.Parallel() + _, err := g.GetAccountHistory("234cb213-ac6f-4ed8-b7b6-e62512930945") + if err == nil { + t.Error("Test failed - GetAccountHistory() error", err) + } +} + +func TestGetHolds(t *testing.T) { + t.Parallel() + _, err := g.GetHolds("234cb213-ac6f-4ed8-b7b6-e62512930945") + if err == nil { + t.Error("Test failed - GetHolds() error", err) + } +} + +func TestPlaceLimitOrder(t *testing.T) { + t.Parallel() + _, err := g.PlaceLimitOrder("", 0, 0, "buy", "", "", "BTC-USD", "", false) + if err == nil { + t.Error("Test failed - PlaceLimitOrder() error", err) + } +} + +func TestPlaceMarketOrder(t *testing.T) { + t.Parallel() + _, err := g.PlaceMarketOrder("", 1, 0, "buy", "BTC-USD", "") + if err == nil { + t.Error("Test failed - PlaceMarketOrder() error", err) + } +} + +func TestCancelOrder(t *testing.T) { + t.Parallel() + err := g.CancelOrder("1337") + if err == nil { + t.Error("Test failed - CancelOrder() error", err) + } +} + +func TestCancelAllOrders(t *testing.T) { + t.Parallel() + _, err := g.CancelAllOrders("BTC-USD") + if err == nil { + t.Error("Test failed - CancelAllOrders() error", err) + } +} + +func TestGetOrders(t *testing.T) { + t.Parallel() + _, err := g.GetOrders([]string{"open", "done"}, "BTC-USD") + if err == nil { + t.Error("Test failed - GetOrders() error", err) + } +} + +func TestGetOrder(t *testing.T) { + t.Parallel() + _, err := g.GetOrder("1337") + if err == nil { + t.Error("Test failed - GetOrders() error", err) + } +} + +func TestGetFills(t *testing.T) { + t.Parallel() + _, err := g.GetFills("1337", "BTC-USD") + if err == nil { + t.Error("Test failed - GetFills() error", err) + } + _, err = g.GetFills("", "") + if err == nil { + t.Error("Test failed - GetFills() error", err) + } +} + +func TestGetFundingRecords(t *testing.T) { + t.Parallel() + _, err := g.GetFundingRecords("rejected") + if err == nil { + t.Error("Test failed - GetFundingRecords() error", err) + } +} + +// func TestRepayFunding(t *testing.T) { +// g.Verbose = true +// _, err := g.RepayFunding("1", "BTC") +// if err != nil { +// t.Error("Test failed - RepayFunding() error", err) +// } +// } + +func TestMarginTransfer(t *testing.T) { //invalid sig issue + t.Parallel() + _, err := g.MarginTransfer(1, "withdraw", "45fa9e3b-00ba-4631-b907-8a98cbdf21be", "BTC") + if err == nil { + t.Error("Test failed - MarginTransfer() error", err) + } +} + +func TestGetPosition(t *testing.T) { + t.Parallel() + _, err := g.GetPosition() + if err == nil { + t.Error("Test failed - GetPosition() error", err) + } +} + +func TestClosePosition(t *testing.T) { + t.Parallel() + _, err := g.ClosePosition(false) + if err == nil { + t.Error("Test failed - ClosePosition() error", err) + } +} + +func TestGetPayMethods(t *testing.T) { + t.Parallel() + _, err := g.GetPayMethods() + if err == nil { + t.Error("Test failed - GetPayMethods() error", err) + } +} + +func TestDepositViaPaymentMethod(t *testing.T) { + t.Parallel() + _, err := g.DepositViaPaymentMethod(1, "BTC", "1337") + if err == nil { + t.Error("Test failed - DepositViaPaymentMethod() error", err) + } +} + +func TestDepositViaCoinbase(t *testing.T) { + t.Parallel() + _, err := g.DepositViaCoinbase(1, "BTC", "1337") + if err == nil { + t.Error("Test failed - DepositViaCoinbase() error", err) + } +} + +func TestWithdrawViaPaymentMethod(t *testing.T) { + t.Parallel() + _, err := g.WithdrawViaPaymentMethod(1, "BTC", "1337") + if err == nil { + t.Error("Test failed - WithdrawViaPaymentMethod() error", err) + } +} + +// func TestWithdrawViaCoinbase(t *testing.T) { // No Route found error +// _, err := g.WithdrawViaCoinbase(1, "BTC", "c13cd0fc-72ca-55e9-843b-b84ef628c198") +// if err != nil { +// t.Error("Test failed - WithdrawViaCoinbase() error", err) +// } +// } + +func TestWithdrawCrypto(t *testing.T) { + t.Parallel() + _, err := g.WithdrawCrypto(1, "BTC", "1337") + if err == nil { + t.Error("Test failed - WithdrawViaCoinbase() error", err) + } +} + +func TestGetCoinbaseAccounts(t *testing.T) { + t.Parallel() + _, err := g.GetCoinbaseAccounts() + if err == nil { + t.Error("Test failed - GetCoinbaseAccounts() error", err) + } +} + +func TestGetReportStatus(t *testing.T) { + t.Parallel() + _, err := g.GetReportStatus("1337") + if err == nil { + t.Error("Test failed - GetReportStatus() error", err) + } +} + +func TestGetTrailingVolume(t *testing.T) { + t.Parallel() + _, err := g.GetTrailingVolume() + if err == nil { + t.Error("Test failed - GetTrailingVolume() error", err) + } +} diff --git a/exchanges/gdax/gdax_types.go b/exchanges/gdax/gdax_types.go index c14894c9..76d0143d 100644 --- a/exchanges/gdax/gdax_types.go +++ b/exchanges/gdax/gdax_types.go @@ -1,13 +1,7 @@ package gdax -type GDAXTicker struct { - TradeID int64 `json:"trade_id"` - Price float64 `json:"price,string"` - Size float64 `json:"size,string"` - Time string `json:"time"` -} - -type GDAXProduct struct { +// Product holds product information +type Product struct { ID string `json:"id"` BaseCurrency string `json:"base_currency"` QuoteCurrency string `json:"quote_currency"` @@ -17,37 +11,16 @@ type GDAXProduct struct { DisplayName string `json:"string"` } -type GDAXOrderL1L2 struct { - Price float64 - Amount float64 - NumOrders float64 +// Ticker holds basic ticker information +type Ticker struct { + TradeID int64 `json:"trade_id"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + Time string `json:"time"` } -type GDAXOrderL3 struct { - Price float64 - Amount float64 - OrderID string -} - -type GDAXOrderbookL1L2 struct { - Sequence int64 `json:"sequence"` - Bids []GDAXOrderL1L2 `json:"bids"` - Asks []GDAXOrderL1L2 `json:"asks"` -} - -type GDAXOrderbookL3 struct { - Sequence int64 `json:"sequence"` - Bids []GDAXOrderL3 `json:"bids"` - Asks []GDAXOrderL3 `json:"asks"` -} - -type GDAXOrderbookResponse struct { - Sequence int64 `json:"sequence"` - Bids [][]interface{} `json:"bids"` - Asks [][]interface{} `json:"asks"` -} - -type GDAXTrade struct { +// Trade holds executed trade information +type Trade struct { TradeID int64 `json:"trade_id"` Price float64 `json:"price,string"` Size float64 `json:"size,string"` @@ -55,37 +28,52 @@ type GDAXTrade struct { Side string `json:"side"` } -type GDAXStats struct { +// History holds historic rate information +type History struct { + Time int64 `json:"time"` + Low float64 `json:"low"` + High float64 `json:"high"` + Open float64 `json:"open"` + Close float64 `json:"close"` + Volume float64 `json:"volume"` +} + +// Stats holds last 24 hr data for gdax +type Stats struct { Open float64 `json:"open,string"` High float64 `json:"high,string"` Low float64 `json:"low,string"` Volume float64 `json:"volume,string"` } -type GDAXCurrency struct { +// Currency holds singular currency product information +type Currency struct { ID string Name string MinSize float64 `json:"min_size,string"` } -type GDAXHistory struct { - Time int64 - Low float64 - High float64 - Open float64 - Close float64 - Volume float64 +// ServerTime holds current requested server time information +type ServerTime struct { + ISO string `json:"iso"` + Epoch float64 `json:"epoch"` } -type GDAXAccountResponse struct { - ID string `json:"id"` - Balance float64 `json:"balance,string"` - Hold float64 `json:"hold,string"` - Available float64 `json:"available,string"` - Currency string `json:"currency"` +// AccountResponse holds the details for the trading accounts +type AccountResponse struct { + ID string `json:"id"` + Currency string `json:"currency"` + Balance float64 `json:"balance,string"` + Available float64 `json:"available,string"` + Hold float64 `json:"hold,string"` + ProfileID string `json:"profile_id"` + MarginEnabled bool `json:"margin_enabled"` + FundedAmount float64 `json:"funded_amount,string"` + DefaultAmount float64 `json:"default_amount,string"` } -type GDAXAccountLedgerResponse struct { +// AccountLedgerResponse holds account history information +type AccountLedgerResponse struct { ID string `json:"id"` CreatedAt string `json:"created_at"` Amount float64 `json:"amount,string"` @@ -94,7 +82,8 @@ type GDAXAccountLedgerResponse struct { Details interface{} `json:"details"` } -type GDAXAccountHolds struct { +// AccountHolds contains the hold information about an account +type AccountHolds struct { ID string `json:"id"` AccountID string `json:"account_id"` CreatedAt string `json:"created_at"` @@ -104,48 +93,182 @@ type GDAXAccountHolds struct { Reference string `json:"ref"` } -type GDAXOrdersResponse struct { - ID string `json:"id"` - Size float64 `json:"size,string"` - Price float64 `json:"price,string"` - ProductID string `json:"product_id"` - Status string `json:"status"` - FilledSize float64 `json:"filled_size,string"` - FillFees float64 `json:"fill_fees,string"` - Settled bool `json:"settled"` - Side string `json:"side"` - CreatedAt string `json:"created_at"` +// GeneralizedOrderResponse is the generalized return type across order +// placement and information collation +type GeneralizedOrderResponse struct { + ID string `json:"id"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + ProductID string `json:"product_id"` + Side string `json:"side"` + Stp string `json:"stp"` + Type string `json:"type"` + TimeInForce string `json:"time_in_force"` + PostOnly bool `json:"post_only"` + CreatedAt string `json:"created_at"` + FillFees float64 `json:"fill_fees,string"` + FilledSize float64 `json:"filled_size,string"` + ExecutedValue float64 `json:"executed_value,string"` + Status string `json:"status"` + Settled bool `json:"settled"` + Funds float64 `json:"funds,string"` + SpecifiedFunds float64 `json:"specified_funds,string"` + DoneReason string `json:"done_reason"` + DoneAt string `json:"done_at"` } -type GDAXOrderResponse struct { - ID string `json:"id"` - Size float64 `json:"size,string"` - Price float64 `json:"price,string"` - DoneReason string `json:"done_reason"` - Status string `json:"status"` - Settled bool `json:"settled"` - FilledSize float64 `json:"filled_size,string"` - ProductID string `json:"product_id"` - FillFees float64 `json:"fill_fees,string"` - Side string `json:"side"` - CreatedAt string `json:"created_at"` - DoneAt string `json:"done_at"` +// Funding holds funding data +type Funding struct { + ID string `json:"id"` + OrderID string `json:"order_id"` + ProfileID string `json:"profile_id"` + Amount float64 `json:"amount,string"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + Currency string `json:"currency"` + RepaidAmount float64 `json:"repaid_amount"` + DefaultAmount float64 `json:"default_amount,string"` + RepaidDefault bool `json:"repaid_default"` } -type GDAXFillResponse struct { - TradeID int `json:"trade_id"` - ProductID string `json:"product_id"` - Price float64 `json:"price,string"` - Size float64 `json:"size,string"` - OrderID string `json:"order_id"` - CreatedAt string `json:"created_at"` - Liquidity string `json:"liquidity"` - Fee float64 `json:"fee,string"` - Settled bool `json:"settled"` - Side string `json:"side"` +// MarginTransfer holds margin transfer details +type MarginTransfer struct { + CreatedAt string `json:"created_at"` + ID string `json:"id"` + UserID string `json:"user_id"` + ProfileID string `json:"profile_id"` + MarginProfileID string `json:"margin_profile_id"` + Type string `json:"type"` + Amount float64 `json:"amount,string"` + Currency string `json:"currency"` + AccountID string `json:"account_id"` + MarginAccountID string `json:"margin_account_id"` + MarginProductID string `json:"margin_product_id"` + Status string `json:"status"` + Nonce int `json:"nonce"` } -type GDAXReportResponse struct { +// AccountOverview holds account information returned from position +type AccountOverview struct { + Status string `json:"status"` + Funding struct { + MaxFundingValue float64 `json:"max_funding_value,string"` + FundingValue float64 `json:"funding_value,string"` + OldestOutstanding struct { + ID string `json:"id"` + OrderID string `json:"order_id"` + CreatedAt string `json:"created_at"` + Currency string `json:"currency"` + AccountID string `json:"account_id"` + Amount float64 `json:"amount,string"` + } `json:"oldest_outstanding"` + } `json:"funding"` + Accounts struct { + LTC Account `json:"LTC"` + ETH Account `json:"ETH"` + USD Account `json:"USD"` + BTC Account `json:"BTC"` + } `json:"accounts"` + MarginCall struct { + Active bool `json:"active"` + Price float64 `json:"price,string"` + Side string `json:"side"` + Size float64 `json:"size,string"` + Funds float64 `json:"funds,string"` + } `json:"margin_call"` + UserID string `json:"user_id"` + ProfileID string `json:"profile_id"` + Position struct { + Type string `json:"type"` + Size float64 `json:"size,string"` + Complement float64 `json:"complement,string"` + MaxSize float64 `json:"max_size,string"` + } `json:"position"` + ProductID string `json:"product_id"` +} + +// Account is a sub-type for account overview +type Account struct { + ID string `json:"id"` + Balance float64 `json:"balance,string"` + Hold float64 `json:"hold,string"` + FundedAmount float64 `json:"funded_amount,string"` + DefaultAmount float64 `json:"default_amount,string"` +} + +// PaymentMethod holds payment method information +type PaymentMethod struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Currency string `json:"currency"` + PrimaryBuy bool `json:"primary_buy"` + PrimarySell bool `json:"primary_sell"` + AllowBuy bool `json:"allow_buy"` + AllowSell bool `json:"allow_sell"` + AllowDeposits bool `json:"allow_deposits"` + AllowWithdraw bool `json:"allow_withdraw"` + Limits struct { + Buy []LimitInfo `json:"buy"` + InstantBuy []LimitInfo `json:"instant_buy"` + Sell []LimitInfo `json:"sell"` + Deposit []LimitInfo `json:"deposit"` + } `json:"limits"` +} + +// LimitInfo is a sub-type for payment method +type LimitInfo struct { + PeriodInDays int `json:"period_in_days"` + Total struct { + Amount float64 `json:"amount,string"` + Currency string `json:"currency"` + } `json:"total"` +} + +// DepositWithdrawalInfo holds returned deposit information +type DepositWithdrawalInfo struct { + ID string `json:"id"` + Amount float64 `json:"amount,string"` + Currency string `json:"currency"` + PayoutAt string `json:"payout_at"` +} + +// CoinbaseAccounts holds coinbase account information +type CoinbaseAccounts struct { + ID string `json:"id"` + Name string `json:"name"` + Balance float64 `json:"balance,string"` + Currency string `json:"currency"` + Type string `json:"type"` + Primary bool `json:"primary"` + Active bool `json:"active"` + WireDepositInformation struct { + AccountNumber string `json:"account_number"` + RoutingNumber string `json:"routing_number"` + BankName string `json:"bank_name"` + BankAddress string `json:"bank_address"` + BankCountry struct { + Code string `json:"code"` + Name string `json:"name"` + } `json:"bank_country"` + AccountName string `json:"account_name"` + AccountAddress string `json:"account_address"` + Reference string `json:"reference"` + } `json:"wire_deposit_information"` + SepaDepositInformation struct { + Iban string `json:"iban"` + Swift string `json:"swift"` + BankName string `json:"bank_name"` + BankAddress string `json:"bank_address"` + BankCountryName string `json:"bank_country_name"` + AccountName string `json:"account_name"` + AccountAddress string `json:"account_address"` + Reference string `json:"reference"` + } `json:"sep_deposit_information"` +} + +// Report holds historical information +type Report struct { ID string `json:"id"` Type string `json:"type"` Status string `json:"status"` @@ -159,12 +282,71 @@ type GDAXReportResponse struct { } `json:"params"` } -type GDAXWebsocketSubscribe struct { +// Volume type contains trailing volume information +type Volume struct { + ProductID string `json:"product_id"` + ExchangeVolume float64 `json:"exchange_volume,string"` + Volume float64 `json:"volume,string"` + RecordedAt string `json:"recorded_at"` +} + +// OrderL1L2 is a type used in layer conversion +type OrderL1L2 struct { + Price float64 + Amount float64 + NumOrders float64 +} + +// OrderL3 is a type used in layer conversion +type OrderL3 struct { + Price float64 + Amount float64 + OrderID string +} + +// OrderbookL1L2 holds level 1 and 2 order book information +type OrderbookL1L2 struct { + Sequence int64 `json:"sequence"` + Bids []OrderL1L2 `json:"bids"` + Asks []OrderL1L2 `json:"asks"` +} + +// OrderbookL3 holds level 3 order book information +type OrderbookL3 struct { + Sequence int64 `json:"sequence"` + Bids []OrderL3 `json:"bids"` + Asks []OrderL3 `json:"asks"` +} + +// OrderbookResponse is a generalized response for order books +type OrderbookResponse struct { + Sequence int64 `json:"sequence"` + Bids [][]interface{} `json:"bids"` + Asks [][]interface{} `json:"asks"` +} + +// FillResponse contains fill information from the exchange +type FillResponse struct { + TradeID int `json:"trade_id"` + ProductID string `json:"product_id"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + OrderID string `json:"order_id"` + CreatedAt string `json:"created_at"` + Liquidity string `json:"liquidity"` + Fee float64 `json:"fee,string"` + Settled bool `json:"settled"` + Side string `json:"side"` +} + +// WebsocketSubscribe takes in subscription information +type WebsocketSubscribe struct { Type string `json:"type"` ProductID string `json:"product_id"` } -type GDAXWebsocketReceived struct { +// WebsocketReceived holds websocket received values +type WebsocketReceived struct { Type string `json:"type"` Time string `json:"time"` Sequence int `json:"sequence"` @@ -174,7 +356,8 @@ type GDAXWebsocketReceived struct { Side string `json:"side"` } -type GDAXWebsocketOpen struct { +// WebsocketOpen collates open orders +type WebsocketOpen struct { Type string `json:"type"` Time string `json:"time"` Sequence int `json:"sequence"` @@ -184,7 +367,8 @@ type GDAXWebsocketOpen struct { Side string `json:"side"` } -type GDAXWebsocketDone struct { +// WebsocketDone holds finished order information +type WebsocketDone struct { Type string `json:"type"` Time string `json:"time"` Sequence int `json:"sequence"` @@ -195,7 +379,8 @@ type GDAXWebsocketDone struct { RemainingSize float64 `json:"remaining_size,string"` } -type GDAXWebsocketMatch struct { +// WebsocketMatch holds match information +type WebsocketMatch struct { Type string `json:"type"` TradeID int `json:"trade_id"` Sequence int `json:"sequence"` @@ -207,7 +392,8 @@ type GDAXWebsocketMatch struct { Side string `json:"side"` } -type GDAXWebsocketChange struct { +// WebsocketChange holds change information +type WebsocketChange struct { Type string `json:"type"` Time string `json:"time"` Sequence int `json:"sequence"` diff --git a/exchanges/gdax/gdax_websocket.go b/exchanges/gdax/gdax_websocket.go index f2656451..423b2dcb 100644 --- a/exchanges/gdax/gdax_websocket.go +++ b/exchanges/gdax/gdax_websocket.go @@ -13,7 +13,7 @@ const ( ) func (g *GDAX) WebsocketSubscribe(product string, conn *websocket.Conn) error { - subscribe := GDAXWebsocketSubscribe{"subscribe", product} + subscribe := WebsocketSubscribe{"subscribe", product} json, err := common.JSONEncode(subscribe) if err != nil { return err @@ -82,35 +82,35 @@ func (g *GDAX) WebsocketClient() { log.Println(string(resp)) break case "received": - received := GDAXWebsocketReceived{} + received := WebsocketReceived{} err := common.JSONDecode(resp, &received) if err != nil { log.Println(err) continue } case "open": - open := GDAXWebsocketOpen{} + open := WebsocketOpen{} err := common.JSONDecode(resp, &open) if err != nil { log.Println(err) continue } case "done": - done := GDAXWebsocketDone{} + done := WebsocketDone{} err := common.JSONDecode(resp, &done) if err != nil { log.Println(err) continue } case "match": - match := GDAXWebsocketMatch{} + match := WebsocketMatch{} err := common.JSONDecode(resp, &match) if err != nil { log.Println(err) continue } case "change": - change := GDAXWebsocketChange{} + change := WebsocketChange{} err := common.JSONDecode(resp, &change) if err != nil { log.Println(err) diff --git a/exchanges/gdax/gdax_wrapper.go b/exchanges/gdax/gdax_wrapper.go index 88d41e43..5e6dafc3 100644 --- a/exchanges/gdax/gdax_wrapper.go +++ b/exchanges/gdax/gdax_wrapper.go @@ -2,20 +2,20 @@ package gdax import ( "log" - "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +// Start starts the GDAX go routine func (g *GDAX) Start() { go g.Run() } +// Run implements the GDAX wrapper func (g *GDAX) Run() { if g.Verbose { log.Printf("%s Websocket: %s. (url: %s).\n", g.GetName(), common.IsEnabled(g.Websocket), GDAX_WEBSOCKET_URL) @@ -37,32 +37,15 @@ func (g *GDAX) Run() { currencies = append(currencies, x.ID[0:3]+x.ID[4:]) } } - err = g.UpdateAvailableCurrencies(currencies) + err = g.UpdateAvailableCurrencies(currencies, false) if err != nil { log.Printf("%s Failed to get config.\n", g.GetName()) } } - - for g.Enabled { - for _, x := range g.EnabledPairs { - currency := pair.NewCurrencyPair(x[0:3], x[3:]) - currency.Delimiter = "-" - go func() { - ticker, err := g.GetTickerPrice(currency) - - if err != nil { - log.Println(err) - return - } - log.Printf("GDAX %s: Last %f High %f Low %f Volume %f\n", exchange.FormatCurrency(currency).String(), ticker.Last, ticker.High, ticker.Low, ticker.Volume) - stats.AddExchangeInfo(g.GetName(), currency.GetFirstCurrency().String(), currency.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - }() - } - time.Sleep(time.Second * g.RESTPollingDelay) - } } -//GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the GDAX exchange +// GetExchangeAccountInfo retrieves balances for all enabled currencies for the +// GDAX exchange func (g *GDAX) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo response.ExchangeName = g.GetName() @@ -81,22 +64,18 @@ func (g *GDAX) GetExchangeAccountInfo() (exchange.AccountInfo, error) { return response, nil } -func (g *GDAX) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - tickerNew, err := ticker.GetTicker(g.GetName(), p) - if err == nil { - return tickerNew, nil +// UpdateTicker updates and returns the ticker for a currency pair +func (g *GDAX) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price + tick, err := g.GetTicker(exchange.FormatExchangeCurrency(g.Name, p).String()) + if err != nil { + return ticker.Price{}, err } - var tickerPrice ticker.TickerPrice - tick, err := g.GetTicker(p.Pair().String()) - if err != nil { - return ticker.TickerPrice{}, err - } - - stats, err := g.GetStats(p.Pair().String()) + stats, err := g.GetStats(exchange.FormatExchangeCurrency(g.Name, p).String()) if err != nil { - return ticker.TickerPrice{}, err + return ticker.Price{}, err } tickerPrice.Pair = p @@ -104,32 +83,46 @@ func (g *GDAX) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { tickerPrice.Last = tick.Price tickerPrice.High = stats.High tickerPrice.Low = stats.Low - ticker.ProcessTicker(g.GetName(), p, tickerPrice) - return tickerPrice, nil + ticker.ProcessTicker(g.GetName(), p, tickerPrice, assetType) + return ticker.GetTicker(g.Name, p, assetType) } -func (g *GDAX) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(g.GetName(), p) - if err == nil { - return ob, nil +// GetTickerPrice returns the ticker for a currency pair +func (g *GDAX) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(g.GetName(), p, assetType) + if err != nil { + return g.UpdateTicker(p, assetType) } + return tickerNew, nil +} - var orderBook orderbook.OrderbookBase - orderbookNew, err := g.GetOrderbook(p.Pair().String(), 2) +// GetOrderbookEx returns orderbook base on the currency pair +func (g *GDAX) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(g.GetName(), p, assetType) + if err == nil { + return g.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (g *GDAX) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + orderbookNew, err := g.GetOrderbook(exchange.FormatExchangeCurrency(g.Name, p).String(), 2) if err != nil { return orderBook, err } - obNew := orderbookNew.(GDAXOrderbookL1L2) + obNew := orderbookNew.(OrderbookL1L2) for x := range obNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.OrderbookItem{Amount: obNew.Bids[x].Amount, Price: obNew.Bids[x].Price}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: obNew.Bids[x].Amount, Price: obNew.Bids[x].Price}) } for x := range obNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.OrderbookItem{Amount: obNew.Bids[x].Amount, Price: obNew.Bids[x].Price}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: obNew.Bids[x].Amount, Price: obNew.Bids[x].Price}) } - orderBook.Pair = p - orderbook.ProcessOrderbook(g.GetName(), p, orderBook) - return orderBook, nil + + orderbook.ProcessOrderbook(g.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(g.Name, p, assetType) } diff --git a/exchanges/gemini/gemini.go b/exchanges/gemini/gemini.go index 654347da..9ff8c6b9 100644 --- a/exchanges/gemini/gemini.go +++ b/exchanges/gemini/gemini.go @@ -7,46 +7,115 @@ import ( "net/url" "strconv" "strings" + "sync" "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( - GEMINI_API_URL = "https://api.gemini.com" - GEMINI_API_VERSION = "1" + geminiAPIURL = "https://api.gemini.com" + geminiSandboxAPIURL = "https://api.sandbox.gemini.com" + geminiAPIVersion = "1" - GEMINI_SYMBOLS = "symbols" - GEMINI_TICKER = "pubticker" - GEMINI_AUCTION = "auction" - GEMINI_AUCTION_HISTORY = "history" - GEMINI_ORDERBOOK = "book" - GEMINI_TRADES = "trades" - GEMINI_ORDERS = "orders" - GEMINI_ORDER_NEW = "order/new" - GEMINI_ORDER_CANCEL = "order/cancel" - GEMINI_ORDER_CANCEL_SESSION = "order/cancel/session" - GEMINI_ORDER_CANCEL_ALL = "order/cancel/all" - GEMINI_ORDER_STATUS = "order/status" - GEMINI_MYTRADES = "mytrades" - GEMINI_BALANCES = "balances" - GEMINI_HEARTBEAT = "heartbeat" + geminiSymbols = "symbols" + geminiTicker = "pubticker" + geminiAuction = "auction" + geminiAuctionHistory = "history" + geminiOrderbook = "book" + geminiTrades = "trades" + geminiOrders = "orders" + geminiOrderNew = "order/new" + geminiOrderCancel = "order/cancel" + geminiOrderCancelSession = "order/cancel/session" + geminiOrderCancelAll = "order/cancel/all" + geminiOrderStatus = "order/status" + geminiMyTrades = "mytrades" + geminiBalances = "balances" + geminiTradeVolume = "tradevolume" + geminiDeposit = "deposit" + geminiNewAddress = "newAddress" + geminiWithdraw = "withdraw/" + geminiHeartbeat = "heartbeat" + + // rate limits per minute + geminiPublicRate = 120 + geminiPrivateRate = 600 + + // rates limits per second + geminiPublicRateSec = 1 + geminiPrivateRateSec = 5 + + // Too many requests returns this + geminiRateError = "429" + + // Assigned API key roles on creation + geminiRoleTrader = "trader" + geminiRoleFundManager = "fundmanager" ) +// SessionID map guides +var ( + sessionAPIKey map[int]string // map[sessionID]APIKEY + sessionAPISecret map[int]string // map[sessionID]APIKEY + sessionRole map[string]string // map[sessionID]Roles + sessionHeartbeat map[int]bool // map[sessionID]RequiresHeartBeat + IsSession bool +) + +// Gemini is the overarching type across the Gemini package, create multiple +// instances with differing APIkeys for segregation of roles for authenticated +// requests & sessions by appending the session function, if sandbox test is +// needed append the sandbox function as well. type Gemini struct { exchange.Base + M sync.Mutex } +// AddSession adds a new session to the gemini base +func (g *Gemini) AddSession(sessionID int, apiKey, apiSecret, role string, needsHeartbeat bool) error { + g.M.Lock() + defer g.M.Unlock() + if sessionAPIKey == nil { + IsSession = true + sessionAPIKey = make(map[int]string) + sessionAPISecret = make(map[int]string) + sessionRole = make(map[string]string) + sessionHeartbeat = make(map[int]bool) + } + _, ok := sessionAPIKey[sessionID] + if ok { + return errors.New("sessionID already being used") + } + + sessionAPIKey[sessionID] = apiKey + sessionAPISecret[sessionID] = apiSecret + sessionRole[apiKey] = role + sessionHeartbeat[sessionID] = needsHeartbeat + + return nil +} + +//return session function? + +// SetDefaults sets package defaults for gemini exchange func (g *Gemini) SetDefaults() { g.Name = "Gemini" g.Enabled = false g.Verbose = false g.Websocket = false g.RESTPollingDelay = 10 + g.RequestCurrencyPairFormat.Delimiter = "" + g.RequestCurrencyPairFormat.Uppercase = true + g.ConfigCurrencyPairFormat.Delimiter = "" + g.ConfigCurrencyPairFormat.Uppercase = true + g.AssetTypes = []string{ticker.Spot} } +// Setup sets exchange configuration paramaters func (g *Gemini) Setup(exch config.ExchangeConfig) { if !exch.Enabled { g.SetEnabled(false) @@ -60,10 +129,48 @@ func (g *Gemini) Setup(exch config.ExchangeConfig) { g.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") g.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") g.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := g.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = g.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } -func (g *Gemini) GetTicker(currency string) (GeminiTicker, error) { +// Session is a session manager for differing APIKeys and roles, use this for all function +// calls in this package +func (g *Gemini) Session(sessionID int) *Gemini { + g.M.Lock() + defer g.M.Unlock() + g.APIUrl = geminiAPIURL + _, ok := sessionAPIKey[sessionID] + if !ok { + return nil + } + g.APIKey = sessionAPIKey[sessionID] + g.APISecret = sessionAPISecret[sessionID] + return g +} + +// Sandbox diverts the apiURL to the sandbox API for testing purposes +func (g *Gemini) Sandbox() *Gemini { + g.APIUrl = geminiSandboxAPIURL + return g +} + +// GetSymbols returns all available symbols for trading +func (g *Gemini) GetSymbols() ([]string, error) { + symbols := []string{} + path := fmt.Sprintf("%s/v%s/%s", geminiAPIURL, geminiAPIVersion, geminiSymbols) + + return symbols, common.SendHTTPGetRequest(path, true, g.Verbose, &symbols) +} + +// GetTicker returns information about recent trading activity for the symbol +func (g *Gemini) GetTicker(currencyPair string) (Ticker, error) { type TickerResponse struct { Ask float64 `json:"ask,string"` @@ -72,11 +179,11 @@ func (g *Gemini) GetTicker(currency string) (GeminiTicker, error) { Volume map[string]interface{} } - ticker := GeminiTicker{} + ticker := Ticker{} resp := TickerResponse{} - path := fmt.Sprintf("%s/v%s/%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_TICKER, currency) + path := fmt.Sprintf("%s/v%s/%s/%s", geminiAPIURL, geminiAPIVersion, geminiTicker, currencyPair) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, g.Verbose, &resp) if err != nil { return ticker, err } @@ -85,7 +192,7 @@ func (g *Gemini) GetTicker(currency string) (GeminiTicker, error) { ticker.Bid = resp.Bid ticker.Last = resp.Last - ticker.Volume.Currency, _ = strconv.ParseFloat(resp.Volume[currency[0:3]].(string), 64) + ticker.Volume.Currency, _ = strconv.ParseFloat(resp.Volume[currencyPair[0:3]].(string), 64) ticker.Volume.USD, _ = strconv.ParseFloat(resp.Volume["USD"].(string), 64) time, _ := resp.Volume["timestamp"].(float64) @@ -94,59 +201,77 @@ func (g *Gemini) GetTicker(currency string) (GeminiTicker, error) { return ticker, nil } -func (g *Gemini) GetSymbols() ([]string, error) { - symbols := []string{} - path := fmt.Sprintf("%s/v%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_SYMBOLS) - err := common.SendHTTPGetRequest(path, true, &symbols) - if err != nil { - return nil, err - } - return symbols, nil +// GetOrderbook returns the current order book, as two arrays, one of bids, and +// one of asks +// +// params - limit_bids or limit_asks [OPTIONAL] default 50, 0 returns all Values +// Type is an integer ie "params.Set("limit_asks", 30)" +func (g *Gemini) GetOrderbook(currencyPair string, params url.Values) (Orderbook, error) { + path := common.EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s", geminiAPIURL, geminiAPIVersion, geminiOrderbook, currencyPair), params) + orderbook := Orderbook{} + + return orderbook, common.SendHTTPGetRequest(path, true, g.Verbose, &orderbook) } -func (g *Gemini) GetAuction(currency string) (GeminiAuction, error) { - path := fmt.Sprintf("%s/v%s/%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_AUCTION, currency) - auction := GeminiAuction{} - err := common.SendHTTPGetRequest(path, true, &auction) - if err != nil { - return auction, err - } - return auction, nil +// GetTrades eturn the trades that have executed since the specified timestamp. +// Timestamps are either seconds or milliseconds since the epoch (1970-01-01). +// +// currencyPair - example "btcusd" +// params -- +// since, timestamp [optional] +// limit_trades integer Optional. The maximum number of trades to return. +// include_breaks boolean Optional. Whether to display broken trades. False by +// default. Can be '1' or 'true' to activate +func (g *Gemini) GetTrades(currencyPair string, params url.Values) ([]Trade, error) { + path := common.EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s", geminiAPIURL, geminiAPIVersion, geminiTrades, currencyPair), params) + trades := []Trade{} + + return trades, common.SendHTTPGetRequest(path, true, g.Verbose, &trades) } -func (g *Gemini) GetAuctionHistory(currency string, params url.Values) ([]GeminiAuctionHistory, error) { - path := common.EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_AUCTION, currency, GEMINI_AUCTION_HISTORY), params) - auctionHist := []GeminiAuctionHistory{} - err := common.SendHTTPGetRequest(path, true, &auctionHist) - if err != nil { - return nil, err - } - return auctionHist, nil +// GetAuction returns auction infomation +func (g *Gemini) GetAuction(currencyPair string) (Auction, error) { + path := fmt.Sprintf("%s/v%s/%s/%s", geminiAPIURL, geminiAPIVersion, geminiAuction, currencyPair) + auction := Auction{} + + return auction, common.SendHTTPGetRequest(path, true, g.Verbose, &auction) } -func (g *Gemini) GetOrderbook(currency string, params url.Values) (GeminiOrderbook, error) { - path := common.EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_ORDERBOOK, currency), params) - orderbook := GeminiOrderbook{} - err := common.SendHTTPGetRequest(path, true, &orderbook) - if err != nil { - return GeminiOrderbook{}, err - } +// GetAuctionHistory returns the auction events, optionally including +// publications of indicative prices, since the specific timestamp. +// +// currencyPair - example "btcusd" +// params -- [optional] +// since - [timestamp] Only returns auction events after the specified +// timestamp. +// limit_auction_results - [integer] The maximum number of auction +// events to return. +// include_indicative - [bool] Whether to include publication of +// indicative prices and quantities. +func (g *Gemini) GetAuctionHistory(currencyPair string, params url.Values) ([]AuctionHistory, error) { + path := common.EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s/%s", geminiAPIURL, geminiAPIVersion, geminiAuction, currencyPair, geminiAuctionHistory), params) + auctionHist := []AuctionHistory{} - return orderbook, nil + return auctionHist, common.SendHTTPGetRequest(path, true, g.Verbose, &auctionHist) } -func (g *Gemini) GetTrades(currency string, params url.Values) ([]GeminiTrade, error) { - path := common.EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_TRADES, currency), params) - trades := []GeminiTrade{} - err := common.SendHTTPGetRequest(path, true, &trades) - if err != nil { - return []GeminiTrade{}, err +func (g *Gemini) isCorrectSession(role string) error { + if !IsSession { + return errors.New("session not set") } - - return trades, nil + if sessionRole[g.APIKey] != role { + return errors.New("incorrect role for APIKEY cannot use this function") + } + return nil } +// NewOrder Only limit orders are supported through the API at present. +// returns order ID if successful func (g *Gemini) NewOrder(symbol string, amount, price float64, side, orderType string) (int64, error) { + if err := g.isCorrectSession(geminiRoleTrader); err != nil { + return 0, err + } + request := make(map[string]interface{}) request["symbol"] = symbol request["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) @@ -154,96 +279,127 @@ func (g *Gemini) NewOrder(symbol string, amount, price float64, side, orderType request["side"] = side request["type"] = orderType - response := GeminiOrder{} - err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_ORDER_NEW, request, &response) + response := Order{} + err := g.SendAuthenticatedHTTPRequest("POST", geminiOrderNew, request, &response) if err != nil { return 0, err } return response.OrderID, nil } -func (g *Gemini) CancelOrder(OrderID int64) (GeminiOrder, error) { +// CancelOrder will cancel an order. If the order is already canceled, the +// message will succeed but have no effect. +func (g *Gemini) CancelOrder(OrderID int64) (Order, error) { request := make(map[string]interface{}) request["order_id"] = OrderID - response := GeminiOrder{} - err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_ORDER_CANCEL, request, &response) + response := Order{} + err := g.SendAuthenticatedHTTPRequest("POST", geminiOrderCancel, request, &response) if err != nil { - return GeminiOrder{}, err + return Order{}, err } return response, nil } -func (g *Gemini) CancelOrders(sessions bool) ([]GeminiOrderResult, error) { - response := []GeminiOrderResult{} - path := GEMINI_ORDER_CANCEL_ALL - if sessions { - path = GEMINI_ORDER_CANCEL_SESSION +// CancelOrders will cancel all outstanding orders created by all sessions owned +// by this account, including interactive orders placed through the UI. If +// sessions = true will only cancel the order that is called on this session +// asssociated with the APIKEY +func (g *Gemini) CancelOrders(CancelBySession bool) (OrderResult, error) { + response := OrderResult{} + path := geminiOrderCancelAll + if CancelBySession { + path = geminiOrderCancelSession } - err := g.SendAuthenticatedHTTPRequest("POST", path, nil, &response) - if err != nil { - return nil, err - } - return response, nil + + return response, g.SendAuthenticatedHTTPRequest("POST", path, nil, &response) } -func (g *Gemini) GetOrderStatus(orderID int64) (GeminiOrder, error) { +// GetOrderStatus returns the status for an order +func (g *Gemini) GetOrderStatus(orderID int64) (Order, error) { request := make(map[string]interface{}) request["order_id"] = orderID - response := GeminiOrder{} - err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_ORDER_STATUS, request, &response) - if err != nil { - return GeminiOrder{}, err - } - return response, nil + response := Order{} + + return response, + g.SendAuthenticatedHTTPRequest("POST", geminiOrderStatus, request, &response) } -func (g *Gemini) GetOrders() ([]GeminiOrder, error) { - response := []GeminiOrder{} - err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_ORDERS, nil, &response) - if err != nil { - return nil, err - } - return response, nil +// GetOrders returns active orders in the market +func (g *Gemini) GetOrders() ([]Order, error) { + response := []Order{} + + return response, + g.SendAuthenticatedHTTPRequest("POST", geminiOrders, nil, &response) } -func (g *Gemini) GetTradeHistory(symbol string, timestamp int64) ([]GeminiTradeHistory, error) { +// GetTradeHistory returns an array of trades that have been on the exchange +// +// currencyPair - example "btcusd" +// timestamp - [optional] Only return trades on or after this timestamp. +func (g *Gemini) GetTradeHistory(currencyPair string, timestamp int64) ([]TradeHistory, error) { + response := []TradeHistory{} request := make(map[string]interface{}) - request["symbol"] = symbol - request["timestamp"] = timestamp + request["symbol"] = currencyPair - response := []GeminiTradeHistory{} - err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_MYTRADES, request, &response) - if err != nil { - return nil, err + if timestamp != 0 { + request["timestamp"] = timestamp } - return response, nil + + return response, + g.SendAuthenticatedHTTPRequest("POST", geminiMyTrades, request, &response) } -func (g *Gemini) GetBalances() ([]GeminiBalance, error) { - response := []GeminiBalance{} - err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_BALANCES, nil, &response) - if err != nil { - return nil, err - } - return response, nil +// GetTradeVolume returns a multi-arrayed volume response +func (g *Gemini) GetTradeVolume() ([][]TradeVolume, error) { + response := [][]TradeVolume{} + + return response, + g.SendAuthenticatedHTTPRequest("POST", geminiTradeVolume, nil, &response) } -func (g *Gemini) PostHeartbeat() (bool, error) { +// GetBalances returns available balances in the supported currencies +func (g *Gemini) GetBalances() ([]Balance, error) { + response := []Balance{} + + return response, + g.SendAuthenticatedHTTPRequest("POST", geminiBalances, nil, &response) +} + +// GetDepositAddress returns a deposit address +func (g *Gemini) GetDepositAddress(depositAddlabel, currency string) (DepositAddress, error) { + response := DepositAddress{} + + return response, + g.SendAuthenticatedHTTPRequest("POST", geminiDeposit+"/"+currency+"/"+geminiNewAddress, nil, &response) +} + +// WithdrawCrypto withdraws crypto currency to a whitelisted address +func (g *Gemini) WithdrawCrypto(address, currency string, amount float64) (WithdrawelAddress, error) { + response := WithdrawelAddress{} + request := make(map[string]interface{}) + request["address"] = address + request["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) + + return response, + g.SendAuthenticatedHTTPRequest("POST", geminiWithdraw+currency, nil, &response) +} + +// PostHeartbeat sends a maintenance heartbeat to the exchange for all heartbeat +// maintaned sessions +func (g *Gemini) PostHeartbeat() (string, error) { type Response struct { - Result bool `json:"result"` + Result string `json:"result"` } - response := Response{} - err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_HEARTBEAT, nil, &response) - if err != nil { - return false, err - } - return response.Result, nil + return response.Result, + g.SendAuthenticatedHTTPRequest("POST", geminiHeartbeat, nil, &response) } +// SendAuthenticatedHTTPRequest sends an authenticated HTTP request to the +// exchange and returns an error func (g *Gemini) SendAuthenticatedHTTPRequest(method, path string, params map[string]interface{}, result interface{}) (err error) { if !g.AuthenticatedAPISupport { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, g.Name) @@ -255,8 +411,9 @@ func (g *Gemini) SendAuthenticatedHTTPRequest(method, path string, params map[st g.Nonce.Inc() } + headers := make(map[string]string) request := make(map[string]interface{}) - request["request"] = fmt.Sprintf("/v%s/%s", GEMINI_API_VERSION, path) + request["request"] = fmt.Sprintf("/v%s/%s", geminiAPIVersion, path) request["nonce"] = g.Nonce.Get() if params != nil { @@ -266,7 +423,6 @@ func (g *Gemini) SendAuthenticatedHTTPRequest(method, path string, params map[st } PayloadJSON, err := common.JSONEncode(request) - if err != nil { return errors.New("SendAuthenticatedHTTPRequest: Unable to JSON request") } @@ -277,21 +433,32 @@ func (g *Gemini) SendAuthenticatedHTTPRequest(method, path string, params map[st PayloadBase64 := common.Base64Encode(PayloadJSON) hmac := common.GetHMAC(common.HashSHA512_384, []byte(PayloadBase64), []byte(g.APISecret)) - headers := make(map[string]string) + headers["X-GEMINI-APIKEY"] = g.APIKey headers["X-GEMINI-PAYLOAD"] = PayloadBase64 headers["X-GEMINI-SIGNATURE"] = common.HexEncodeToString(hmac) - resp, err := common.SendHTTPRequest(method, GEMINI_API_URL+path, headers, strings.NewReader("")) + resp, err := common.SendHTTPRequest(method, g.APIUrl+"/v1/"+path, headers, strings.NewReader("")) + if err != nil { + return err + } if g.Verbose { log.Printf("Received raw: \n%s\n", resp) } - err = common.JSONDecode([]byte(resp), &result) + captureErr := ErrorCapture{} + if err = common.JSONDecode([]byte(resp), &captureErr); err == nil { + if len(captureErr.Message) != 0 || len(captureErr.Result) != 0 || len(captureErr.Reason) != 0 { + if captureErr.Result != "ok" { + return errors.New(captureErr.Message) + } + } + } + err = common.JSONDecode([]byte(resp), &result) if err != nil { - return errors.New("unable to JSON Unmarshal response") + return err } return nil diff --git a/exchanges/gemini/gemini_test.go b/exchanges/gemini/gemini_test.go new file mode 100644 index 00000000..4701f52a --- /dev/null +++ b/exchanges/gemini/gemini_test.go @@ -0,0 +1,224 @@ +package gemini + +import ( + "net/url" + "testing" + + "github.com/thrasher-/gocryptotrader/config" +) + +var ( + g Gemini +) + +// Please enter sandbox API keys & assigned roles for better testing procedures + +const ( + apiKey1 = "" + apiSecret1 = "" + apiKeyRole1 = "" + sessionHeartBeat1 = false + + apiKey2 = "" + apiSecret2 = "" + apiKeyRole2 = "" + sessionHeartBeat2 = false +) + +func TestAddSession(t *testing.T) { + err := g.AddSession(1, apiKey1, apiSecret1, apiKeyRole1, true) + if err != nil { + t.Error("Test failed - AddSession() error") + } + err = g.AddSession(1, apiKey1, apiSecret1, apiKeyRole1, true) + if err == nil { + t.Error("Test failed - AddSession() error") + } + err = g.AddSession(2, apiKey2, apiSecret2, apiKeyRole2, false) + if err != nil { + t.Error("Test failed - AddSession() error") + } +} + +func TestSetDefaults(t *testing.T) { + g.SetDefaults() +} + +func TestSetup(t *testing.T) { + cfg := config.GetConfig() + cfg.LoadConfig("../../testdata/configtest.dat") + geminiConfig, err := cfg.GetExchangeConfig("Gemini") + if err != nil { + t.Error("Test Failed - Gemini Setup() init error") + } + + geminiConfig.AuthenticatedAPISupport = true + + g.Setup(geminiConfig) +} + +func TestSession(t *testing.T) { + t.Parallel() + if g.Session(1) == nil { + t.Error("Test Failed - Session() error") + } + if g.Session(1337) != nil { + t.Error("Test Failed - Session() error") + } +} + +func TestSandbox(t *testing.T) { + t.Parallel() + g.APIUrl = geminiAPIURL + if g.Sandbox().APIUrl != geminiSandboxAPIURL { + t.Error("Test Failed - Sandbox() error") + } +} + +func TestGetSymbols(t *testing.T) { + t.Parallel() + _, err := g.GetSymbols() + if err != nil { + t.Error("Test Failed - GetSymbols() error", err) + } +} + +func TestGetTicker(t *testing.T) { + t.Parallel() + _, err := g.GetTicker("BTCUSD") + if err != nil { + t.Error("Test Failed - GetTicker() error", err) + } + _, err = g.GetTicker("bla") + if err == nil { + t.Error("Test Failed - GetTicker() error", err) + } +} + +func TestGetOrderbook(t *testing.T) { + t.Parallel() + _, err := g.GetOrderbook("btcusd", url.Values{}) + if err != nil { + t.Error("Test Failed - GetOrderbook() error", err) + } +} + +func TestGetTrades(t *testing.T) { + t.Parallel() + _, err := g.GetTrades("btcusd", url.Values{}) + if err != nil { + t.Error("Test Failed - GetTrades() error", err) + } +} + +func TestGetAuction(t *testing.T) { + t.Parallel() + _, err := g.GetAuction("btcusd") + if err != nil { + t.Error("Test Failed - GetAuction() error", err) + } +} + +func TestGetAuctionHistory(t *testing.T) { + t.Parallel() + _, err := g.GetAuctionHistory("btcusd", url.Values{}) + if err != nil { + t.Error("Test Failed - GetAuctionHistory() error", err) + } +} + +func TestNewOrder(t *testing.T) { + t.Parallel() + _, err := g.Session(1).Sandbox().NewOrder("btcusd", 1, 4500, "buy", "exchange limit") + if err == nil { + t.Error("Test Failed - NewOrder() error", err) + } + _, err = g.Session(2).Sandbox().NewOrder("btcusd", 1, 4500, "buy", "exchange limit") + if err == nil { + t.Error("Test Failed - NewOrder() error", err) + } +} + +func TestCancelOrder(t *testing.T) { + t.Parallel() + _, err := g.Session(1).Sandbox().CancelOrder(1337) + if err == nil { + t.Error("Test Failed - CancelOrder() error", err) + } +} + +func TestCancelOrders(t *testing.T) { + t.Parallel() + _, err := g.Session(1).Sandbox().CancelOrders(false) + if err == nil { + t.Error("Test Failed - CancelOrders() error", err) + } + _, err = g.Session(2).Sandbox().CancelOrders(true) + if err == nil { + t.Error("Test Failed - CancelOrders() error", err) + } +} + +func TestGetOrderStatus(t *testing.T) { + t.Parallel() + _, err := g.Session(1).Sandbox().GetOrderStatus(1337) + if err == nil { + t.Error("Test Failed - GetOrderStatus() error", err) + } +} + +func TestGetOrders(t *testing.T) { + t.Parallel() + _, err := g.Session(1).Sandbox().GetOrders() + if err == nil { + t.Error("Test Failed - GetOrders() error", err) + } +} + +func TestGetTradeHistory(t *testing.T) { + t.Parallel() + _, err := g.Session(1).Sandbox().GetTradeHistory("btcusd", 0) + if err == nil { + t.Error("Test Failed - GetTradeHistory() error", err) + } +} + +func TestGetTradeVolume(t *testing.T) { + t.Parallel() + _, err := g.Session(1).Sandbox().GetTradeVolume() + if err == nil { + t.Error("Test Failed - GetTradeVolume() error", err) + } +} + +func TestGetBalances(t *testing.T) { + t.Parallel() + _, err := g.Session(1).Sandbox().GetBalances() + if err == nil { + t.Error("Test Failed - GetBalances() error", err) + } +} + +func TestGetDepositAddress(t *testing.T) { + t.Parallel() + _, err := g.Session(1).Sandbox().GetDepositAddress("LOL123", "btc") + if err == nil { + t.Error("Test Failed - GetDepositAddress() error", err) + } +} + +func TestWithdrawCrypto(t *testing.T) { + t.Parallel() + _, err := g.Session(1).Sandbox().WithdrawCrypto("LOL123", "btc", 1) + if err == nil { + t.Error("Test Failed - WithdrawCrypto() error", err) + } +} + +func TestPostHeartbeat(t *testing.T) { + t.Parallel() + _, err := g.Session(1).Sandbox().PostHeartbeat() + if err == nil { + t.Error("Test Failed - PostHeartbeat() error", err) + } +} diff --git a/exchanges/gemini/gemini_types.go b/exchanges/gemini/gemini_types.go index 68eaebbd..a6cd4aed 100644 --- a/exchanges/gemini/gemini_types.go +++ b/exchanges/gemini/gemini_types.go @@ -1,66 +1,7 @@ package gemini -type GeminiOrderbookEntry struct { - Price float64 `json:"price,string"` - Amount float64 `json:"amount,string"` -} - -type GeminiOrderbook struct { - Bids []GeminiOrderbookEntry `json:"bids"` - Asks []GeminiOrderbookEntry `json:"asks"` -} - -type GeminiTrade struct { - Timestamp int64 `json:"timestamp"` - TID int64 `json:"tid"` - Price float64 `json:"price"` - Amount float64 `json:"amount"` - Side string `json:"taker"` -} - -type GeminiOrder struct { - OrderID int64 `json:"order_id"` - ClientOrderID string `json:"client_order_id"` - Symbol string `json:"symbol"` - Exchange string `json:"exchange"` - Price float64 `json:"price,string"` - AvgExecutionPrice float64 `json:"avg_execution_price,string"` - Side string `json:"side"` - Type string `json:"type"` - Timestamp int64 `json:"timestamp"` - TimestampMS int64 `json:"timestampms"` - IsLive bool `json:"is_live"` - IsCancelled bool `json:"is_cancelled"` - WasForced bool `json:"was_forced"` - ExecutedAmount float64 `json:"executed_amount,string"` - RemainingAmount float64 `json:"remaining_amount,string"` - OriginalAmount float64 `json:"original_amount,string"` -} - -type GeminiOrderResult struct { - Result bool `json:"result"` -} - -type GeminiTradeHistory struct { - Price float64 `json:"price"` - Amount float64 `json:"amount"` - Timestamp int64 `json:"timestamp"` - TimestampMS int64 `json:"timestampms"` - Type string `json:"type"` - FeeCurrency string `json:"fee_currency"` - FeeAmount float64 `json:"fee_amount"` - TID int64 `json:"tid"` - OrderID int64 `json:"order_id"` - ClientOrderID string `json:"client_order_id"` -} - -type GeminiBalance struct { - Currency string `json:"currency"` - Amount float64 `json:"amount"` - Available float64 `json:"available"` -} - -type GeminiTicker struct { +// Ticker holds returned ticker data from the exchange +type Ticker struct { Ask float64 `json:"ask,string"` Bid float64 `json:"bid,string"` Last float64 `json:"last,string"` @@ -71,16 +12,47 @@ type GeminiTicker struct { } } -type GeminiAuction struct { - LastAuctionPrice float64 `json:"last_auction_price,string"` - LastAuctionQuantity float64 `json:"last_auction_quantity,string"` - LastHighestBidPrice float64 `json:"last_highest_bid_price,string"` - LastLowestAskPrice float64 `json:"last_lowest_ask_price,string"` - NextUpdateMS int64 `json:"next_update_ms"` - NextAuctionMS int64 `json:"next_auction_ms"` - LastAuctionEID int64 `json:"last_auction_eid"` +// Orderbook contains orderbook information for both bid and ask side +type Orderbook struct { + Bids []OrderbookEntry `json:"bids"` + Asks []OrderbookEntry `json:"asks"` } -type GeminiAuctionHistory struct { + +// OrderbookEntry subtype of orderbook information +type OrderbookEntry struct { + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` +} + +// Trade holds trade history for a specific currency pair +type Trade struct { + Timestamp int64 `json:"timestamp"` + Timestampms int64 `json:"timestampms"` + TID int64 `json:"tid"` + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` + Exchange string `json:"exchange"` + Side string `json:"type"` +} + +// Auction is generalized response type +type Auction struct { + LastAuctionEID int64 `json:"last_auction_eid"` + ClosedUntilMs int64 `json:"closed_until_ms"` + LastAuctionPrice float64 `json:"last_auction_price,string"` + LastAuctionQuantity float64 `json:"last_auction_quantity,string"` + LastHighestBidPrice float64 `json:"last_highest_bid_price,string"` + LastLowestAskPrice float64 `json:"last_lowest_ask_price,string"` + NextAuctionMS int64 `json:"next_auction_ms"` + NextUpdateMS int64 `json:"next_update_ms"` + MostRecentIndicativePrice float64 `json:"most_recent_indicative_price,string"` + MostRecentIndicativeQuantity float64 `json:"most_recent_indicative_quantity,string"` + MostRecentHighestBidPrice float64 `json:"most_recent_highest_bid_price,string"` + MostRecentLowestAskPrice float64 `json:"most_recent_lowest_ask_price,string"` +} + +// AuctionHistory holds auction history information +type AuctionHistory struct { AuctionID int64 `json:"auction_id"` AuctionPrice float64 `json:"auction_price,string"` AuctionQuantity float64 `json:"auction_quantity,string"` @@ -92,3 +64,102 @@ type GeminiAuctionHistory struct { TimestampMS int64 `json:"timestampms"` EventType string `json:"event_type"` } + +// OrderResult holds cancelled order information +type OrderResult struct { + Result string `json:"result"` + Details struct { + CancelledOrders []string `json:"cancelledOrders"` + CancelRejects []string `json:"cancelRejects"` + } `json:"details"` +} + +// Order contains order information +type Order struct { + OrderID int64 `json:"order_id,string"` + ID int64 `json:"id,string"` + ClientOrderID string `json:"client_order_id"` + Symbol string `json:"symbol"` + Exchange string `json:"exchange"` + Price float64 `json:"price,string"` + AvgExecutionPrice float64 `json:"avg_execution_price,string"` + Side string `json:"side"` + Type string `json:"type"` + Timestamp int64 `json:"timestamp,string"` + TimestampMS int64 `json:"timestampms"` + IsLive bool `json:"is_live"` + IsCancelled bool `json:"is_cancelled"` + IsHidden bool `json:"is_hidden"` + Options []string `json:"options"` + WasForced bool `json:"was_forced"` + ExecutedAmount float64 `json:"executed_amount,string"` + RemainingAmount float64 `json:"remaining_amount,string"` + OriginalAmount float64 `json:"original_amount,string"` +} + +// TradeHistory holds trade history information +type TradeHistory struct { + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` + Timestamp int64 `json:"timestamp"` + TimestampMS int64 `json:"timestampms"` + Type string `json:"type"` + FeeCurrency string `json:"fee_currency"` + FeeAmount float64 `json:"fee_amount,string"` + TID int64 `json:"tid"` + OrderID int64 `json:"order_id,string"` + Exchange string `json:"exchange"` + IsAuctionFilled bool `json:"is_auction_fill"` + ClientOrderID string `json:"client_order_id"` +} + +// TradeVolume holds Volume information +type TradeVolume struct { + AccountID int64 `json:"account_id"` + Symbol string `json:"symbol"` + BaseCurrency string `json:"base_currency"` + NotionalCurrency string `json:"notional_currency"` + Date string `json:"date_date"` + TotalVolumeBase float64 `json:"total_volume_base"` + MakerBuySellRatio float64 `json:"maker_buy_sell_ratio"` + BuyMakerBase float64 `json:"buy_maker_base"` + BuyMakerNotional float64 `json:"buy_maker_notional"` + BuyMakerCount float64 `json:"buy_maker_count"` + SellMakerBase float64 `json:"sell_maker_base"` + SellMakerNotional float64 `json:"sell_maker_notional"` + SellMakerCount float64 `json:"sell_maker_count"` + BuyTakerBase float64 `json:"buy_taker_base"` + BuyTakerNotional float64 `json:"buy_taker_notional"` + BuyTakerCount float64 `json:"buy_taker_count"` + SellTakerBase float64 `json:"sell_taker_base"` + SellTakerNotional float64 `json:"sell_taker_notional"` + SellTakerCount float64 `json:"sell_taker_count"` +} + +// Balance is a simple balance type +type Balance struct { + Currency string `json:"currency"` + Amount float64 `json:"amount,string"` + Available float64 `json:"available,string"` +} + +// DepositAddress holds assigned deposit address for a specific currency +type DepositAddress struct { + Currency string `json:"currency"` + Address string `json:"address"` + Label string `json:"label"` +} + +// WithdrawelAddress holds withdrawel information +type WithdrawelAddress struct { + Address string `json:"address"` + Amount float64 `json:"amount"` + TXHash string `json:"txHash"` +} + +// ErrorCapture is a generlized error response from the server +type ErrorCapture struct { + Result string `json:"result"` + Reason string `json:"reason"` + Message string `json:"message"` +} diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index 272f5eb9..143c8e88 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -3,19 +3,19 @@ package gemini import ( "log" "net/url" - "time" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +// Start starts the Gemini go routine func (g *Gemini) Start() { go g.Run() } +// Run implements the Gemini wrapper func (g *Gemini) Run() { if g.Verbose { log.Printf("%s polling delay: %ds.\n", g.GetName(), g.RESTPollingDelay) @@ -26,34 +26,19 @@ func (g *Gemini) Run() { if err != nil { log.Printf("%s Failed to get available symbols.\n", g.GetName()) } else { - err = g.UpdateAvailableCurrencies(exchangeProducts) + err = g.UpdateAvailableCurrencies(exchangeProducts, false) if err != nil { log.Printf("%s Failed to get config.\n", g.GetName()) } } - - for g.Enabled { - for _, x := range g.EnabledPairs { - currency := pair.NewCurrencyPair(x[0:3], x[3:]) - go func() { - ticker, err := g.GetTickerPrice(currency) - if err != nil { - log.Println(err) - return - } - log.Printf("Gemini %s Last %f Bid %f Ask %f Volume %f\n", exchange.FormatCurrency(currency).String(), ticker.Last, ticker.Bid, ticker.Ask, ticker.Volume) - stats.AddExchangeInfo(g.GetName(), currency.GetFirstCurrency().String(), currency.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - }() - } - time.Sleep(time.Second * g.RESTPollingDelay) - } } -//GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the Gemini exchange -func (e *Gemini) GetExchangeAccountInfo() (exchange.AccountInfo, error) { +// GetExchangeAccountInfo Retrieves balances for all enabled currencies for the +// Gemini exchange +func (g *Gemini) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.ExchangeName = e.GetName() - accountBalance, err := e.GetBalances() + response.ExchangeName = g.GetName() + accountBalance, err := g.GetBalances() if err != nil { return response, err } @@ -62,19 +47,14 @@ func (e *Gemini) GetExchangeAccountInfo() (exchange.AccountInfo, error) { exchangeCurrency.CurrencyName = accountBalance[i].Currency exchangeCurrency.TotalValue = accountBalance[i].Amount exchangeCurrency.Hold = accountBalance[i].Available - response.Currencies = append(response.Currencies, exchangeCurrency) } return response, nil } -func (g *Gemini) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - tickerNew, err := ticker.GetTicker(g.GetName(), p) - if err == nil { - return tickerNew, nil - } - - var tickerPrice ticker.TickerPrice +// UpdateTicker updates and returns the ticker for a currency pair +func (g *Gemini) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price tick, err := g.GetTicker(p.Pair().String()) if err != nil { return tickerPrice, err @@ -84,31 +64,44 @@ func (g *Gemini) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) tickerPrice.Bid = tick.Bid tickerPrice.Last = tick.Last tickerPrice.Volume = tick.Volume.USD - ticker.ProcessTicker(g.GetName(), p, tickerPrice) - return tickerPrice, nil + ticker.ProcessTicker(g.GetName(), p, tickerPrice, assetType) + return ticker.GetTicker(g.Name, p, assetType) } -func (g *Gemini) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(g.GetName(), p) - if err == nil { - return ob, nil +// GetTickerPrice returns the ticker for a currency pair +func (g *Gemini) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(g.GetName(), p, assetType) + if err != nil { + return g.UpdateTicker(p, assetType) } + return tickerNew, nil +} - var orderBook orderbook.OrderbookBase +// GetOrderbookEx returns orderbook base on the currency pair +func (g *Gemini) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(g.GetName(), p, assetType) + if err == nil { + return g.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (g *Gemini) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base orderbookNew, err := g.GetOrderbook(p.Pair().String(), url.Values{}) if err != nil { return orderBook, err } for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.OrderbookItem{Amount: orderbookNew.Bids[x].Amount, Price: orderbookNew.Bids[x].Price}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: orderbookNew.Bids[x].Amount, Price: orderbookNew.Bids[x].Price}) } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.OrderbookItem{Amount: orderbookNew.Asks[x].Amount, Price: orderbookNew.Asks[x].Price}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: orderbookNew.Asks[x].Amount, Price: orderbookNew.Asks[x].Price}) } - orderBook.Pair = p - orderbook.ProcessOrderbook(g.GetName(), p, orderBook) - return orderBook, nil + orderbook.ProcessOrderbook(g.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(g.Name, p, assetType) } diff --git a/exchanges/huobi/huobi.go b/exchanges/huobi/huobi.go index f3edab71..9d31a34a 100644 --- a/exchanges/huobi/huobi.go +++ b/exchanges/huobi/huobi.go @@ -11,6 +11,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( @@ -29,6 +30,11 @@ func (h *HUOBI) SetDefaults() { h.Verbose = false h.Websocket = false h.RESTPollingDelay = 10 + h.RequestCurrencyPairFormat.Delimiter = "" + h.RequestCurrencyPairFormat.Uppercase = false + h.ConfigCurrencyPairFormat.Delimiter = "" + h.ConfigCurrencyPairFormat.Uppercase = true + h.AssetTypes = []string{ticker.Spot} } func (h *HUOBI) Setup(exch config.ExchangeConfig) { @@ -44,6 +50,14 @@ func (h *HUOBI) Setup(exch config.ExchangeConfig) { h.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") h.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") h.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := h.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = h.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } @@ -53,8 +67,8 @@ func (h *HUOBI) GetFee() float64 { func (h *HUOBI) GetTicker(symbol string) (HuobiTicker, error) { resp := HuobiTickerResponse{} - path := fmt.Sprintf("http://api.huobi.com/staticmarket/ticker_%s_json.js", symbol) - err := common.SendHTTPGetRequest(path, true, &resp) + path := fmt.Sprintf("https://api.huobi.com/staticmarket/ticker_%s_json.js", symbol) + err := common.SendHTTPGetRequest(path, true, h.Verbose, &resp) if err != nil { return HuobiTicker{}, err @@ -63,9 +77,9 @@ func (h *HUOBI) GetTicker(symbol string) (HuobiTicker, error) { } func (h *HUOBI) GetOrderBook(symbol string) (HuobiOrderbook, error) { - path := fmt.Sprintf("http://api.huobi.com/staticmarket/depth_%s_json.js", symbol) + path := fmt.Sprintf("https://api.huobi.com/staticmarket/depth_%s_json.js", symbol) resp := HuobiOrderbook{} - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, h.Verbose, &resp) if err != nil { return resp, err } diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index a17c28d2..31042c6d 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -2,21 +2,20 @@ package huobi import ( "log" - "time" "github.com/thrasher-/gocryptotrader/common" - "github.com/thrasher-/gocryptotrader/currency" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +// Start starts the HUOBI go routine func (h *HUOBI) Start() { go h.Run() } +// Run implements the HUOBI wrapper func (h *HUOBI) Run() { if h.Verbose { log.Printf("%s Websocket: %s (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket), HUOBI_SOCKETIO_ADDRESS) @@ -27,35 +26,11 @@ func (h *HUOBI) Run() { if h.Websocket { go h.WebsocketClient() } - - for h.Enabled { - for _, x := range h.EnabledPairs { - curr := pair.NewCurrencyPair(x[0:3], x[3:]) - go func() { - ticker, err := h.GetTickerPrice(curr) - if err != nil { - log.Println(err) - return - } - HuobiLastUSD, _ := currency.ConvertCurrency(ticker.Last, "CNY", "USD") - HuobiHighUSD, _ := currency.ConvertCurrency(ticker.High, "CNY", "USD") - HuobiLowUSD, _ := currency.ConvertCurrency(ticker.Low, "CNY", "USD") - log.Printf("Huobi %s: Last %f (%f) High %f (%f) Low %f (%f) Volume %f\n", exchange.FormatCurrency(curr).String(), HuobiLastUSD, ticker.Last, HuobiHighUSD, ticker.High, HuobiLowUSD, ticker.Low, ticker.Volume) - stats.AddExchangeInfo(h.GetName(), curr.GetFirstCurrency().String(), curr.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - stats.AddExchangeInfo(h.GetName(), curr.GetFirstCurrency().String(), "USD", HuobiLastUSD, ticker.Volume) - }() - } - time.Sleep(time.Second * h.RESTPollingDelay) - } } -func (h *HUOBI) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - tickerNew, err := ticker.GetTicker(h.GetName(), p) - if err == nil { - return tickerNew, nil - } - - var tickerPrice ticker.TickerPrice +// UpdateTicker updates and returns the ticker for a currency pair +func (h *HUOBI) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price tick, err := h.GetTicker(p.GetFirstCurrency().Lower().String()) if err != nil { return tickerPrice, err @@ -67,17 +42,31 @@ func (h *HUOBI) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) tickerPrice.Last = tick.Last tickerPrice.Volume = tick.Vol tickerPrice.High = tick.High - ticker.ProcessTicker(h.GetName(), p, tickerPrice) - return tickerPrice, nil + ticker.ProcessTicker(h.GetName(), p, tickerPrice, assetType) + return ticker.GetTicker(h.Name, p, assetType) } -func (h *HUOBI) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(h.GetName(), p) - if err == nil { - return ob, nil +// GetTickerPrice returns the ticker for a currency pair +func (h *HUOBI) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(h.GetName(), p, assetType) + if err != nil { + return h.UpdateTicker(p, assetType) } + return tickerNew, nil +} - var orderBook orderbook.OrderbookBase +// GetOrderbookEx returns orderbook base on the currency pair +func (h *HUOBI) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(h.GetName(), p, assetType) + if err == nil { + return h.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (h *HUOBI) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base orderbookNew, err := h.GetOrderBook(p.GetFirstCurrency().Lower().String()) if err != nil { return orderBook, err @@ -85,22 +74,22 @@ func (h *HUOBI) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, er for x := range orderbookNew.Bids { data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.OrderbookItem{Amount: data[1], Price: data[0]}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data[1], Price: data[0]}) } for x := range orderbookNew.Asks { data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.OrderbookItem{Amount: data[1], Price: data[0]}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data[1], Price: data[0]}) } - orderBook.Pair = p - orderbook.ProcessOrderbook(h.GetName(), p, orderBook) - return orderBook, nil + + orderbook.ProcessOrderbook(h.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(h.Name, p, assetType) } -//TODO: retrieve HUOBI balance info -//GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the HUOBI exchange -func (e *HUOBI) GetExchangeAccountInfo() (exchange.AccountInfo, error) { +//GetExchangeAccountInfo retrieves balances for all enabled currencies for the +// HUOBI exchange - to-do +func (h *HUOBI) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.ExchangeName = e.GetName() + response.ExchangeName = h.GetName() return response, nil } diff --git a/exchanges/itbit/itbit.go b/exchanges/itbit/itbit.go index 47a97532..49fc986a 100644 --- a/exchanges/itbit/itbit.go +++ b/exchanges/itbit/itbit.go @@ -12,17 +12,30 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( - ITBIT_API_URL = "https://api.itbit.com/v1" - ITBIT_API_VERSION = "1" + itbitAPIURL = "https://api.itbit.com/v1" + itbitAPIVersion = "1" + itbitMarkets = "markets" + itbitOrderbook = "order_book" + itbitTicker = "ticker" + itbitWallets = "wallets" + itbitBalances = "balances" + itbitTrades = "trades" + itbitFundingHistory = "funding_history" + itbitOrders = "orders" + itbitCryptoDeposits = "cryptocurrency_deposits" + itbitWalletTransfer = "wallet_transfers" ) +// ItBit is the overarching type across the ItBit package type ItBit struct { exchange.Base } +// SetDefaults sets the defaults for the exchange func (i *ItBit) SetDefaults() { i.Name = "ITBIT" i.Enabled = false @@ -31,8 +44,14 @@ func (i *ItBit) SetDefaults() { i.Verbose = false i.Websocket = false i.RESTPollingDelay = 10 + i.RequestCurrencyPairFormat.Delimiter = "" + i.RequestCurrencyPairFormat.Uppercase = true + i.ConfigCurrencyPairFormat.Delimiter = "" + i.ConfigCurrencyPairFormat.Uppercase = true + i.AssetTypes = []string{ticker.Spot} } +// Setup sets the exchange paramaters from exchange config func (i *ItBit) Setup(exch config.ExchangeConfig) { if !exch.Enabled { i.SetEnabled(false) @@ -46,9 +65,18 @@ func (i *ItBit) Setup(exch config.ExchangeConfig) { i.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") i.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") i.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := i.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = i.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } +// GetFee returns the maker or taker fee func (i *ItBit) GetFee(maker bool) float64 { if maker { return i.MakerFee @@ -56,98 +84,103 @@ func (i *ItBit) GetFee(maker bool) float64 { return i.TakerFee } -func (i *ItBit) GetTicker(currency string) (Ticker, error) { - path := ITBIT_API_URL + "/markets/" + currency + "/ticker" - var itbitTicker Ticker - err := common.SendHTTPGetRequest(path, true, &itbitTicker) - if err != nil { - return Ticker{}, err - } - return itbitTicker, nil +// GetTicker returns ticker info for a specified market. +// currencyPair - example "XBTUSD" "XBTSGD" "XBTEUR" +func (i *ItBit) GetTicker(currencyPair string) (Ticker, error) { + var response Ticker + path := fmt.Sprintf("%s/%s/%s/%s", itbitAPIURL, itbitMarkets, currencyPair, itbitTicker) + + return response, + common.SendHTTPGetRequest(path, true, i.Verbose, &response) } -func (i *ItBit) GetOrderbook(currency string) (OrderbookResponse, error) { +// GetOrderbook returns full order book for the specified market. +// currencyPair - example "XBTUSD" "XBTSGD" "XBTEUR" +func (i *ItBit) GetOrderbook(currencyPair string) (OrderbookResponse, error) { response := OrderbookResponse{} - path := ITBIT_API_URL + "/markets/" + currency + "/order_book" - err := common.SendHTTPGetRequest(path, true, &response) - if err != nil { - return OrderbookResponse{}, err - } - return response, nil + path := fmt.Sprintf("%s/%s/%s/%s", itbitAPIURL, itbitMarkets, currencyPair, itbitOrderbook) + + return response, + common.SendHTTPGetRequest(path, true, i.Verbose, &response) } -func (i *ItBit) GetTradeHistory(currency, timestamp string) bool { - req := "/trades?since=" + timestamp - err := common.SendHTTPGetRequest(ITBIT_API_URL+"markets/"+currency+req, true, nil) - if err != nil { - log.Println(err) - return false - } - return true +// GetTradeHistory returns recent trades for a specified market. +// +// currencyPair - example "XBTUSD" "XBTSGD" "XBTEUR" +// timestamp - matchNumber, only executions after this will be returned +func (i *ItBit) GetTradeHistory(currencyPair, timestamp string) (Trades, error) { + response := Trades{} + req := "trades?since=" + timestamp + path := fmt.Sprintf("%s/%s/%s/%s", itbitAPIURL, itbitMarkets, currencyPair, req) + + return response, + common.SendHTTPGetRequest(path, true, i.Verbose, &response) } -func (i *ItBit) GetWallets(params url.Values) { +// GetWallets returns information about all wallets associated with the account. +// +// params -- +// page - [optional] page to return example 1. default 1 +// perPage - [optional] items per page example 50, default 50 max 50 +func (i *ItBit) GetWallets(params url.Values) ([]Wallet, error) { + resp := []Wallet{} params.Set("userId", i.ClientID) - path := "/wallets?" + params.Encode() + path := fmt.Sprintf("/%s?%s", itbitWallets, params.Encode()) - err := i.SendAuthenticatedHTTPRequest("GET", path, nil) - - if err != nil { - log.Println(err) - } + return resp, i.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) } -func (i *ItBit) CreateWallet(walletName string) { - path := "/wallets" +// CreateWallet creates a new wallet with a specified name. +func (i *ItBit) CreateWallet(walletName string) (Wallet, error) { + resp := Wallet{} params := make(map[string]interface{}) params["userId"] = i.ClientID params["name"] = walletName - err := i.SendAuthenticatedHTTPRequest("POST", path, params) - - if err != nil { - log.Println(err) - } + return resp, + i.SendAuthenticatedHTTPRequest("POST", "/"+itbitWallets, params, &resp) } -func (i *ItBit) GetWallet(walletID string) { - path := "/wallets/" + walletID - err := i.SendAuthenticatedHTTPRequest("GET", path, nil) +// GetWallet returns wallet information by walletID +func (i *ItBit) GetWallet(walletID string) (Wallet, error) { + resp := Wallet{} + path := fmt.Sprintf("/%s/%s", itbitWallets, walletID) - if err != nil { - log.Println(err) - } + return resp, i.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) } -func (i *ItBit) GetWalletBalance(walletID, currency string) { - path := "/wallets/ " + walletID + "/balances/" + currency - err := i.SendAuthenticatedHTTPRequest("GET", path, nil) +// GetWalletBalance returns balance information for a specific currency in a +// wallet. +func (i *ItBit) GetWalletBalance(walletID, currency string) (Balance, error) { + resp := Balance{} + path := fmt.Sprintf("/%s/%s/%s/%s", itbitWallets, walletID, itbitBalances, currency) - if err != nil { - log.Println(err) - } + return resp, i.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) } -func (i *ItBit) GetWalletTrades(walletID string, params url.Values) { - path := common.EncodeURLValues("/wallets/"+walletID+"/trades", params) - err := i.SendAuthenticatedHTTPRequest("GET", path, nil) +// GetWalletTrades returns all trades for a specified wallet. +func (i *ItBit) GetWalletTrades(walletID string, params url.Values) (Records, error) { + resp := Records{} + url := fmt.Sprintf("/%s/%s/%s", itbitWallets, walletID, itbitTrades) + path := common.EncodeURLValues(url, params) - if err != nil { - log.Println(err) - } + return resp, i.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) } -func (i *ItBit) GetWalletOrders(walletID string, params url.Values) { - path := common.EncodeURLValues("/wallets/"+walletID+"/orders", params) - err := i.SendAuthenticatedHTTPRequest("GET", path, nil) +// GetFundingHistory returns all funding history for a specified wallet. +func (i *ItBit) GetFundingHistory(walletID string, params url.Values) (FundingRecords, error) { + resp := FundingRecords{} + url := fmt.Sprintf("/%s/%s/%s", itbitWallets, walletID, itbitFundingHistory) + path := common.EncodeURLValues(url, params) - if err != nil { - log.Println(err) - } + return resp, i.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) } -func (i *ItBit) PlaceWalletOrder(walletID, side, orderType, currency string, amount, price float64, instrument string, clientRef string) { - path := "/wallets/" + walletID + "/orders" +// PlaceOrder places a new order +func (i *ItBit) PlaceOrder(walletID, side, orderType, currency string, amount, price float64, instrument, clientRef string) (Order, error) { + resp := Order{} + path := fmt.Sprintf("/%s/%s/%s", itbitWallets, walletID, itbitOrders) + params := make(map[string]interface{}) params["side"] = side params["type"] = orderType @@ -160,85 +193,58 @@ func (i *ItBit) PlaceWalletOrder(walletID, side, orderType, currency string, amo params["clientOrderIdentifier"] = clientRef } - err := i.SendAuthenticatedHTTPRequest("POST", path, params) - - if err != nil { - log.Println(err) - } + return resp, i.SendAuthenticatedHTTPRequest("POST", path, params, &resp) } -func (i *ItBit) GetWalletOrder(walletID, orderID string) { - path := "/wallets/" + walletID + "/orders/" + orderID - err := i.SendAuthenticatedHTTPRequest("GET", path, nil) +// GetOrder returns an order by id. +func (i *ItBit) GetOrder(walletID string, params url.Values) (Order, error) { + resp := Order{} + url := fmt.Sprintf("/%s/%s/%s", itbitWallets, walletID, itbitOrders) + path := common.EncodeURLValues(url, params) - if err != nil { - log.Println(err) - } + return resp, i.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) } -func (i *ItBit) CancelWalletOrder(walletID, orderID string) { - path := "/wallets/" + walletID + "/orders/" + orderID - err := i.SendAuthenticatedHTTPRequest("DELETE", path, nil) +// CancelOrder cancels and open order. *This is not a guarantee that the order +// has been cancelled!* +func (i *ItBit) CancelOrder(walletID, orderID string) error { + path := fmt.Sprintf("/%s/%s/%s/%s", itbitWallets, walletID, itbitOrders, orderID) - if err != nil { - log.Println(err) - } + return i.SendAuthenticatedHTTPRequest("DELETE", path, nil, nil) } -func (i *ItBit) PlaceWithdrawalRequest(walletID, currency, address string, amount float64) { - path := "/wallets/" + walletID + "/cryptocurrency_withdrawals" - params := make(map[string]interface{}) - params["currency"] = currency - params["amount"] = amount - params["address"] = address - - err := i.SendAuthenticatedHTTPRequest("POST", path, params) - - if err != nil { - log.Println(err) - } -} - -func (i *ItBit) GetDepositAddress(walletID, currency string) { - path := "/wallets/" + walletID + "/cryptocurrency_deposits" +// GetDepositAddress returns a deposit address to send cryptocurrency to. +func (i *ItBit) GetDepositAddress(walletID, currency string) (CryptoCurrencyDeposit, error) { + resp := CryptoCurrencyDeposit{} + path := fmt.Sprintf("/%s/%s/%s", itbitWallets, walletID, itbitCryptoDeposits) params := make(map[string]interface{}) params["currency"] = currency - err := i.SendAuthenticatedHTTPRequest("POST", path, params) - - if err != nil { - log.Println(err) - } + return resp, i.SendAuthenticatedHTTPRequest("POST", path, params, &resp) } -func (i *ItBit) WalletTransfer(walletID, sourceWallet, destWallet string, amount float64, currency string) { - path := "/wallets/" + walletID + "/wallet_transfers" +// WalletTransfer transfers funds between wallets. +func (i *ItBit) WalletTransfer(walletID, sourceWallet, destWallet string, amount float64, currency string) (WalletTransfer, error) { + resp := WalletTransfer{} + path := fmt.Sprintf("/%s/%s/%s", itbitWallets, walletID, itbitWalletTransfer) + params := make(map[string]interface{}) params["sourceWalletId"] = sourceWallet params["destinationWalletId"] = destWallet params["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) params["currencyCode"] = currency - err := i.SendAuthenticatedHTTPRequest("POST", path, params) - - if err != nil { - log.Println(err) - } + return resp, i.SendAuthenticatedHTTPRequest("POST", path, params, &resp) } -func (i *ItBit) SendAuthenticatedHTTPRequest(method string, path string, params map[string]interface{}) (err error) { +// SendAuthenticatedHTTPRequest sends an authenticated request to itBit +func (i *ItBit) SendAuthenticatedHTTPRequest(method string, path string, params map[string]interface{}, result interface{}) error { if !i.AuthenticatedAPISupport { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, i.Name) } - if i.Nonce.Get() == 0 { - i.Nonce.Set(time.Now().UnixNano()) - } else { - i.Nonce.Inc() - } - request := make(map[string]interface{}) - url := ITBIT_API_URL + path + url := itbitAPIURL + path if params != nil { for key, value := range params { @@ -247,12 +253,13 @@ func (i *ItBit) SendAuthenticatedHTTPRequest(method string, path string, params } PayloadJSON := []byte("") + var err error if params != nil { - PayloadJSON, err = common.JSONEncode(request) + PayloadJSON, err = common.JSONEncode(request) if err != nil { - return errors.New("SendAuthenticatedHTTPRequest: Unable to JSON Marshal request") + return err } if i.Verbose { @@ -260,26 +267,37 @@ func (i *ItBit) SendAuthenticatedHTTPRequest(method string, path string, params } } - message, err := common.JSONEncode([]string{method, url, string(PayloadJSON), i.Nonce.String(), i.Nonce.String()[0:13]}) + nonce := i.Nonce.GetValue(i.Name, false).String() + timestamp := strconv.FormatInt(time.Now().UnixNano()/1000000, 10) + + message, err := common.JSONEncode([]string{method, url, string(PayloadJSON), nonce, timestamp}) if err != nil { - log.Println(err) - return + return err } - hash := common.GetSHA256([]byte(i.Nonce.String() + string(message))) + hash := common.GetSHA256([]byte(nonce + string(message))) hmac := common.GetHMAC(common.HashSHA512, []byte(url+string(hash)), []byte(i.APISecret)) signature := common.Base64Encode(hmac) headers := make(map[string]string) headers["Authorization"] = i.ClientID + ":" + signature - headers["X-Auth-Timestamp"] = i.Nonce.String()[0:13] - headers["X-Auth-Nonce"] = i.Nonce.String() + headers["X-Auth-Timestamp"] = timestamp + headers["X-Auth-Nonce"] = nonce headers["Content-Type"] = "application/json" resp, err := common.SendHTTPRequest(method, url, headers, bytes.NewBuffer([]byte(PayloadJSON))) + if err != nil { + return err + } if i.Verbose { log.Printf("Received raw: \n%s\n", resp) } - return nil + + errCapture := GeneralReturn{} + if err := common.JSONDecode([]byte(resp), &errCapture); err == nil { + return errors.New(errCapture.Description) + } + + return common.JSONDecode([]byte(resp), result) } diff --git a/exchanges/itbit/itbit_test.go b/exchanges/itbit/itbit_test.go new file mode 100644 index 00000000..88c51689 --- /dev/null +++ b/exchanges/itbit/itbit_test.go @@ -0,0 +1,145 @@ +package itbit + +import ( + "net/url" + "testing" + + "github.com/thrasher-/gocryptotrader/config" +) + +var i ItBit + +// Please provide your own keys to do proper testing +const ( + apiKey = "" + apiSecret = "" + clientID = "" +) + +func TestSetDefaults(t *testing.T) { + i.SetDefaults() +} + +func TestSetup(t *testing.T) { + cfg := config.GetConfig() + cfg.LoadConfig("../../testdata/configtest.dat") + itbitConfig, err := cfg.GetExchangeConfig("ITBIT") + if err != nil { + t.Error("Test Failed - Gemini Setup() init error") + } + + itbitConfig.AuthenticatedAPISupport = true + itbitConfig.APIKey = apiKey + itbitConfig.APISecret = apiSecret + itbitConfig.ClientID = clientID + + i.Setup(itbitConfig) +} + +func TestGetFee(t *testing.T) { + t.Parallel() + if i.GetFee(true) != -0.1 || i.GetFee(false) != 0.5 { + t.Error("Test Failed - GetFee() error") + } +} + +func TestGetTicker(t *testing.T) { + t.Parallel() + _, err := i.GetTicker("XBTUSD") + if err != nil { + t.Error("Test Failed - GetTicker() error", err) + } +} + +func TestGetOrderbook(t *testing.T) { + t.Parallel() + _, err := i.GetOrderbook("XBTSGD") + if err != nil { + t.Error("Test Failed - GetOrderbook() error", err) + } +} + +func TestGetTradeHistory(t *testing.T) { + t.Parallel() + _, err := i.GetTradeHistory("XBTUSD", "0") + if err != nil { + t.Error("Test Failed - GetTradeHistory() error", err) + } +} + +func TestGetWallets(t *testing.T) { + _, err := i.GetWallets(url.Values{}) + if err == nil { + t.Error("Test Failed - GetWallets() error", err) + } +} + +func TestCreateWallet(t *testing.T) { + _, err := i.CreateWallet("test") + if err == nil { + t.Error("Test Failed - CreateWallet() error", err) + } +} + +func TestGetWallet(t *testing.T) { + _, err := i.GetWallet("1337") + if err == nil { + t.Error("Test Failed - GetWallet() error", err) + } +} + +func TestGetWalletBalance(t *testing.T) { + _, err := i.GetWalletBalance("1337", "XRT") + if err == nil { + t.Error("Test Failed - GetWalletBalance() error", err) + } +} + +func TestGetWalletTrades(t *testing.T) { + _, err := i.GetWalletTrades("1337", url.Values{}) + if err == nil { + t.Error("Test Failed - GetWalletTrades() error", err) + } +} + +func TestGetFundingHistory(t *testing.T) { + _, err := i.GetFundingHistory("1337", url.Values{}) + if err == nil { + t.Error("Test Failed - GetFundingHistory() error", err) + } +} + +func TestPlaceOrder(t *testing.T) { + _, err := i.PlaceOrder("1337", "buy", "limit", "USD", 1, 0.2, "banjo", "sauce") + if err == nil { + t.Error("Test Failed - PlaceOrder() error", err) + } +} + +func TestGetOrder(t *testing.T) { + _, err := i.GetOrder("1337", url.Values{}) + if err == nil { + t.Error("Test Failed - GetOrder() error", err) + } +} + +func TestCancelOrder(t *testing.T) { + err := i.CancelOrder("1337", "1337order") + if err == nil { + t.Error("Test Failed - CancelOrder() error", err) + } +} + +func TestGetDepositAddress(t *testing.T) { + _, err := i.GetDepositAddress("1337", "AUD") + if err == nil { + t.Error("Test Failed - GetDepositAddress() error", err) + } +} + +func TestWalletTransfer(t *testing.T) { + _, err := i.WalletTransfer("1337", "mywallet", "anotherwallet", 200, "USD") + if err == nil { + t.Error("Test Failed - WalletTransfer() error", err) + } +} diff --git a/exchanges/itbit/itbit_types.go b/exchanges/itbit/itbit_types.go index 9613404d..8adce526 100644 --- a/exchanges/itbit/itbit_types.go +++ b/exchanges/itbit/itbit_types.go @@ -1,26 +1,145 @@ package itbit -type Ticker struct { - Pair string - Bid float64 `json:",string"` - BidAmt float64 `json:",string"` - Ask float64 `json:",string"` - AskAmt float64 `json:",string"` - LastPrice float64 `json:",string"` - LastAmt float64 `json:",string"` - Volume24h float64 `json:",string"` - VolumeToday float64 `json:",string"` - High24h float64 `json:",string"` - Low24h float64 `json:",string"` - HighToday float64 `json:",string"` - LowToday float64 `json:",string"` - OpenToday float64 `json:",string"` - VwapToday float64 `json:",string"` - Vwap24h float64 `json:",string"` - ServertimeUTC string +// GeneralReturn is a generalized return type to capture any errors +type GeneralReturn struct { + Code int `json:"code"` + Description string `json:"description"` + RequestID string `json:"requestId"` } +// Ticker holds returned ticker information +type Ticker struct { + Pair string `json:"pair"` + Bid float64 `json:"bid,string"` + BidAmt float64 `json:"bidAmt,string"` + Ask float64 `json:"ask,string"` + AskAmt float64 `json:"askAmt,string"` + LastPrice float64 `json:"lastPrice,string"` + LastAmt float64 `json:"lastAmt,string"` + Volume24h float64 `json:"volume24h,string"` + VolumeToday float64 `json:"volumeToday,string"` + High24h float64 `json:"high24h,string"` + Low24h float64 `json:"low24h,string"` + HighToday float64 `json:"highToday,string"` + LowToday float64 `json:"lowToday,string"` + OpenToday float64 `json:"openToday,string"` + VwapToday float64 `json:"vwapToday,string"` + Vwap24h float64 `json:"vwap24h,string"` + ServertimeUTC string `json:"serverTimeUTC"` +} + +// OrderbookResponse contains multi-arrayed strings of bid and ask side +// information type OrderbookResponse struct { Bids [][]string `json:"bids"` Asks [][]string `json:"asks"` } + +// Trades holds recent trades with associated information +type Trades struct { + RecentTrades []struct { + Timestamp string `json:"timestamp"` + MatchNumber int64 `json:"matchNumber"` + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` + } `json:"recentTrades"` +} + +// Wallet contains specific wallet information +type Wallet struct { + ID string `json:"id"` + UserID string `json:"userId"` + Name string `json:"name"` + Balances []Balance `json:"balances"` +} + +// Balance is a sub type holding balance information +type Balance struct { + Currency string `json:"currency"` + AvailableBalance float64 `json:"availableBalance,string"` + TotalBalance float64 `json:"totalBalance,string"` +} + +// Records embodies records of trade history information +type Records struct { + TotalNumberOfRecords int `json:"totalNumberOfRecords,string"` + CurrentPageNumber int `json:"currentPageNumber,string"` + LatestExecutedID int64 `json:"latestExecutionId,string"` + RecordsPerPage int `json:"recordsPerPage,string"` + TradingHistory []TradeHistory `json:"tradingHistory"` +} + +// TradeHistory stores historic trade values +type TradeHistory struct { + OrderID string `json:"orderId"` + Timestamp string `json:"timestamp"` + Instrument string `json:"instrument"` + Direction string `json:"direction"` + CurrencyOne string `json:"currency1"` + CurrencyOneAmount float64 `json:"currency1Amount,string"` + CurrencyTwo string `json:"currency2"` + CurrencyTwoAmount float64 `json:"currency2Amount"` + Rate float64 `json:"rate,string"` + CommissionPaid float64 `json:"commissionPaid,string"` + CommissionCurrency string `json:"commissionCurrency"` + RebatesApplied float64 `json:"rebatesApplied,string"` + RebateCurrency string `json:"rebateCurrency"` +} + +// FundingRecords embodies records of fund history information +type FundingRecords struct { + TotalNumberOfRecords int `json:"totalNumberOfRecords,string"` + CurrentPageNumber int `json:"currentPageNumber,string"` + LatestExecutedID int64 `json:"latestExecutionId,string"` + RecordsPerPage int `json:"recordsPerPage,string"` + FundingHistory []FundHistory `json:"fundingHistory"` +} + +// FundHistory stores historic funding transactions +type FundHistory struct { + BankName string `json:"bankName"` + WithdrawalID int64 `json:"withdrawalId"` + HoldingPeriodCompletionDate string `json:"holdingPeriodCompletionDate"` + DestinationAddress string `json:"destinationAddress"` + TxnHash string `json:"txnHash"` + Time string `json:"time"` + Currency string `json:"currency"` + TransactionType string `json:"transactionType"` + Amount float64 `json:"amount,string"` + WalletName string `json:"walletName"` + Status string `json:"status"` +} + +// Order holds order information +type Order struct { + ID string `json:"id"` + WalletID string `json:"walletId"` + Side string `json:"side"` + Instrument string `json:"instrument"` + Type string `json:"type"` + Currency string `json:"currency"` + Amount float64 `json:"amount,string"` + Price float64 `json:"price,string"` + AmountFilled float64 `json:"amountFilled,string"` + VolumeWeightedAveragePrice float64 `json:"volumeWeightedAveragePrice,string"` + CreatedTime string `json:"createdTime"` + Status string `json:"Status"` + Metadata interface{} `json:"metadata"` + ClientOrderIdentifier string `json:"clientOrderIdentifier"` +} + +// CryptoCurrencyDeposit holds information about a new wallet +type CryptoCurrencyDeposit struct { + ID int `json:"id"` + WalletID string `json:"walletID"` + DepositAddress string `json:"depositAddress"` + Metadata interface{} `json:"metadata"` +} + +// WalletTransfer holds wallet transfer information +type WalletTransfer struct { + SourceWalletID string `json:"sourceWalletId"` + DestinationWalletID string `json:"destinationWalletId"` + Amount float64 `json:"amount,string"` + CurrencyCode string `json:"currencyCode"` +} diff --git a/exchanges/itbit/itbit_wrapper.go b/exchanges/itbit/itbit_wrapper.go index deb80fa2..30335616 100644 --- a/exchanges/itbit/itbit_wrapper.go +++ b/exchanges/itbit/itbit_wrapper.go @@ -3,49 +3,31 @@ package itbit import ( "log" "strconv" - "time" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +// Start starts the ItBit go routine func (i *ItBit) Start() { go i.Run() } + +// Run implements the ItBit wrapper func (i *ItBit) Run() { if i.Verbose { log.Printf("%s polling delay: %ds.\n", i.GetName(), i.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", i.GetName(), len(i.EnabledPairs), i.EnabledPairs) } - - for i.Enabled { - for _, x := range i.EnabledPairs { - currency := pair.NewCurrencyPair(x[0:3], x[3:]) - go func() { - ticker, err := i.GetTickerPrice(currency) - if err != nil { - log.Println(err) - return - } - log.Printf("ItBit %s: Last %f High %f Low %f Volume %f\n", exchange.FormatCurrency(currency).String(), ticker.Last, ticker.High, ticker.Low, ticker.Volume) - stats.AddExchangeInfo(i.GetName(), currency.GetFirstCurrency().String(), currency.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - }() - } - time.Sleep(time.Second * i.RESTPollingDelay) - } } -func (i *ItBit) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - tickerNew, err := ticker.GetTicker(i.GetName(), p) - if err == nil { - return tickerNew, nil - } - - var tickerPrice ticker.TickerPrice - tick, err := i.GetTicker(p.Pair().String()) +// UpdateTicker updates and returns the ticker for a currency pair +func (i *ItBit) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price + tick, err := i.GetTicker(exchange.FormatExchangeCurrency(i.Name, + p).String()) if err != nil { return tickerPrice, err } @@ -57,18 +39,33 @@ func (i *ItBit) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) tickerPrice.High = tick.High24h tickerPrice.Low = tick.Low24h tickerPrice.Volume = tick.Volume24h - ticker.ProcessTicker(i.GetName(), p, tickerPrice) - return tickerPrice, nil + ticker.ProcessTicker(i.GetName(), p, tickerPrice, assetType) + return ticker.GetTicker(i.Name, p, assetType) } -func (i *ItBit) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(i.GetName(), p) - if err == nil { - return ob, nil +// GetTickerPrice returns the ticker for a currency pair +func (i *ItBit) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(i.GetName(), p, assetType) + if err != nil { + return i.UpdateTicker(p, assetType) } + return tickerNew, nil +} - var orderBook orderbook.OrderbookBase - orderbookNew, err := i.GetOrderbook(p.Pair().String()) +// GetOrderbookEx returns orderbook base on the currency pair +func (i *ItBit) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(i.GetName(), p, assetType) + if err == nil { + return i.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (i *ItBit) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + orderbookNew, err := i.GetOrderbook(exchange.FormatExchangeCurrency(i.Name, + p).String()) if err != nil { return orderBook, err } @@ -83,7 +80,7 @@ func (i *ItBit) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, er if err != nil { log.Println(err) } - orderBook.Bids = append(orderBook.Bids, orderbook.OrderbookItem{Amount: amount, Price: price}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: amount, Price: price}) } for x := range orderbookNew.Asks { @@ -96,15 +93,15 @@ func (i *ItBit) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, er if err != nil { log.Println(err) } - orderBook.Asks = append(orderBook.Asks, orderbook.OrderbookItem{Amount: amount, Price: price}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: amount, Price: price}) } - orderBook.Pair = p - orderbook.ProcessOrderbook(i.GetName(), p, orderBook) - return orderBook, nil + + orderbook.ProcessOrderbook(i.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(i.Name, p, assetType) } -//TODO Get current holdings from ItBit -//GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the ItBit exchange +// GetExchangeAccountInfo retrieves balances for all enabled currencies for the +//ItBit exchange - to-do func (i *ItBit) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo response.ExchangeName = i.GetName() diff --git a/exchanges/kraken/kraken.go b/exchanges/kraken/kraken.go index 0be7fe3c..418881ef 100644 --- a/exchanges/kraken/kraken.go +++ b/exchanges/kraken/kraken.go @@ -12,6 +12,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( @@ -55,6 +56,12 @@ func (k *Kraken) SetDefaults() { k.Websocket = false k.RESTPollingDelay = 10 k.Ticker = make(map[string]KrakenTicker) + k.RequestCurrencyPairFormat.Delimiter = "" + k.RequestCurrencyPairFormat.Uppercase = true + k.RequestCurrencyPairFormat.Separator = "," + k.ConfigCurrencyPairFormat.Delimiter = "" + k.ConfigCurrencyPairFormat.Uppercase = true + k.AssetTypes = []string{ticker.Spot} } func (k *Kraken) Setup(exch config.ExchangeConfig) { @@ -70,6 +77,14 @@ func (k *Kraken) Setup(exch config.ExchangeConfig) { k.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") k.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") k.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := k.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = k.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } @@ -84,7 +99,7 @@ func (k *Kraken) GetFee(cryptoTrade bool) float64 { func (k *Kraken) GetServerTime() error { var result interface{} path := fmt.Sprintf("%s/%s/public/%s", KRAKEN_API_URL, KRAKEN_API_VERSION, KRAKEN_SERVER_TIME) - err := common.SendHTTPGetRequest(path, true, &result) + err := common.SendHTTPGetRequest(path, true, k.Verbose, &result) if err != nil { return err @@ -97,7 +112,7 @@ func (k *Kraken) GetServerTime() error { func (k *Kraken) GetAssets() error { var result interface{} path := fmt.Sprintf("%s/%s/public/%s", KRAKEN_API_URL, KRAKEN_API_VERSION, KRAKEN_ASSETS) - err := common.SendHTTPGetRequest(path, true, &result) + err := common.SendHTTPGetRequest(path, true, k.Verbose, &result) if err != nil { return err @@ -115,7 +130,7 @@ func (k *Kraken) GetAssetPairs() (map[string]KrakenAssetPairs, error) { response := Response{} path := fmt.Sprintf("%s/%s/public/%s", KRAKEN_API_URL, KRAKEN_API_VERSION, KRAKEN_ASSET_PAIRS) - err := common.SendHTTPGetRequest(path, true, &response) + err := common.SendHTTPGetRequest(path, true, k.Verbose, &response) if err != nil { return nil, err @@ -135,7 +150,7 @@ func (k *Kraken) GetTicker(symbol string) error { resp := Response{} path := fmt.Sprintf("%s/%s/public/%s?%s", KRAKEN_API_URL, KRAKEN_API_VERSION, KRAKEN_TICKER, values.Encode()) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, k.Verbose, &resp) if err != nil { return err @@ -168,7 +183,7 @@ func (k *Kraken) GetOHLC(symbol string) error { var result interface{} path := fmt.Sprintf("%s/%s/public/%s?%s", KRAKEN_API_URL, KRAKEN_API_VERSION, KRAKEN_OHLC, values.Encode()) - err := common.SendHTTPGetRequest(path, true, &result) + err := common.SendHTTPGetRequest(path, true, k.Verbose, &result) if err != nil { return err @@ -178,20 +193,62 @@ func (k *Kraken) GetOHLC(symbol string) error { return nil } -func (k *Kraken) GetDepth(symbol string) error { +// GetDepth returns the orderbook for a particular currency +func (k *Kraken) GetDepth(symbol string) (Orderbook, error) { values := url.Values{} values.Set("pair", symbol) var result interface{} + var ob Orderbook path := fmt.Sprintf("%s/%s/public/%s?%s", KRAKEN_API_URL, KRAKEN_API_VERSION, KRAKEN_DEPTH, values.Encode()) - err := common.SendHTTPGetRequest(path, true, &result) + err := common.SendHTTPGetRequest(path, true, k.Verbose, &result) if err != nil { - return err + return ob, err } - log.Println(result) - return nil + data := result.(map[string]interface{}) + orderbookData := data["result"].(map[string]interface{}) + + var bidsData []interface{} + var asksData []interface{} + for _, y := range orderbookData { + yData := y.(map[string]interface{}) + bidsData = yData["bids"].([]interface{}) + asksData = yData["asks"].([]interface{}) + } + + processOrderbook := func(data []interface{}) ([]OrderbookBase, error) { + var result []OrderbookBase + for x := range data { + entry := data[x].([]interface{}) + + price, err := strconv.ParseFloat(entry[0].(string), 64) + if err != nil { + return nil, err + } + + amount, err := strconv.ParseFloat(entry[1].(string), 64) + if err != nil { + return nil, err + } + + result = append(result, OrderbookBase{Price: price, Amount: amount}) + } + return result, nil + } + + ob.Bids, err = processOrderbook(bidsData) + if err != nil { + return ob, err + } + + ob.Asks, err = processOrderbook(asksData) + if err != nil { + return ob, err + } + + return ob, nil } func (k *Kraken) GetTrades(symbol string) error { @@ -200,7 +257,7 @@ func (k *Kraken) GetTrades(symbol string) error { var result interface{} path := fmt.Sprintf("%s/%s/public/%s?%s", KRAKEN_API_URL, KRAKEN_API_VERSION, KRAKEN_TRADES, values.Encode()) - err := common.SendHTTPGetRequest(path, true, &result) + err := common.SendHTTPGetRequest(path, true, k.Verbose, &result) if err != nil { return err @@ -216,7 +273,7 @@ func (k *Kraken) GetSpread(symbol string) { var result interface{} path := fmt.Sprintf("%s/%s/public/%s?%s", KRAKEN_API_URL, KRAKEN_API_VERSION, KRAKEN_SPREAD, values.Encode()) - err := common.SendHTTPGetRequest(path, true, &result) + err := common.SendHTTPGetRequest(path, true, k.Verbose, &result) if err != nil { log.Println(err) diff --git a/exchanges/kraken/kraken_types.go b/exchanges/kraken/kraken_types.go index f1ec460d..007009fc 100644 --- a/exchanges/kraken/kraken_types.go +++ b/exchanges/kraken/kraken_types.go @@ -31,6 +31,18 @@ type KrakenTicker struct { Open float64 } +// OrderbookBase stores the orderbook price and amount data +type OrderbookBase struct { + Price float64 + Amount float64 +} + +// Orderbook stores the bids and asks orderbook data +type Orderbook struct { + Bids []OrderbookBase + Asks []OrderbookBase +} + type KrakenTickerResponse struct { Ask []string `json:"a"` Bid []string `json:"b"` diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 7c5626bc..30410649 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -2,20 +2,19 @@ package kraken import ( "log" - "time" - "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +// Start starts the Kraken go routine func (k *Kraken) Start() { go k.Run() } +// Run implements the Kraken wrapper func (k *Kraken) Run() { if k.Verbose { log.Printf("%s polling delay: %ds.\n", k.GetName(), k.RESTPollingDelay) @@ -30,50 +29,87 @@ func (k *Kraken) Run() { for _, v := range assetPairs { exchangeProducts = append(exchangeProducts, v.Altname) } - err = k.UpdateAvailableCurrencies(exchangeProducts) + err = k.UpdateAvailableCurrencies(exchangeProducts, false) if err != nil { log.Printf("%s Failed to get config.\n", k.GetName()) } } +} - for k.Enabled { - err := k.GetTicker(common.JoinStrings(k.EnabledPairs, ",")) - if err != nil { - log.Println(err) - } else { - for _, x := range k.EnabledPairs { - ticker := k.Ticker[x] - log.Printf("Kraken %s Last %f High %f Low %f Volume %f\n", x, ticker.Last, ticker.High, ticker.Low, ticker.Volume) - stats.AddExchangeInfo(k.GetName(), x[0:3], x[3:], ticker.Last, ticker.Volume) - } - } - time.Sleep(time.Second * k.RESTPollingDelay) +// UpdateTicker updates and returns the ticker for a currency pair +func (k *Kraken) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price + pairs := k.GetEnabledCurrencies() + pairsCollated, err := exchange.GetAndFormatExchangeCurrencies(k.Name, pairs) + if err != nil { + return tickerPrice, err + } + err = k.GetTicker(pairsCollated.String()) + if err != nil { + return tickerPrice, err } -} -//This will return the TickerPrice struct when tickers are completed here.. -func (k *Kraken) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - var tickerPrice ticker.TickerPrice - /* - ticker, err := i.GetTicker(currency) - if err != nil { - log.Println(err) - return tickerPrice + for _, x := range pairs { + var tp ticker.Price + tick, ok := k.Ticker[x.Pair().String()] + if !ok { + continue } - tickerPrice.Ask = ticker.Ask - tickerPrice.Bid = ticker.Bid - */ - return tickerPrice, nil + + tp.Pair = x + tp.Last = tick.Last + tp.Ask = tick.Ask + tp.Bid = tick.Bid + tp.High = tick.High + tp.Low = tick.Low + tp.Volume = tick.Volume + ticker.ProcessTicker(k.GetName(), x, tp, assetType) + } + return ticker.GetTicker(k.GetName(), p, assetType) } -func (k *Kraken) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - return orderbook.OrderbookBase{}, nil +// GetTickerPrice returns the ticker for a currency pair +func (k *Kraken) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(k.GetName(), p, assetType) + if err != nil { + return k.UpdateTicker(p, assetType) + } + return tickerNew, nil } -//TODO: Retrieve Kraken info -//GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the Kraken exchange -func (e *Kraken) GetExchangeAccountInfo() (exchange.AccountInfo, error) { +// GetOrderbookEx returns orderbook base on the currency pair +func (k *Kraken) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(k.GetName(), p, assetType) + if err == nil { + return k.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (k *Kraken) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + orderbookNew, err := k.GetDepth(exchange.FormatExchangeCurrency(k.GetName(), p).String()) + if err != nil { + return orderBook, err + } + + for x := range orderbookNew.Bids { + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: orderbookNew.Bids[x].Amount, Price: orderbookNew.Bids[x].Price}) + } + + for x := range orderbookNew.Asks { + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: orderbookNew.Asks[x].Amount, Price: orderbookNew.Asks[x].Price}) + } + + orderbook.ProcessOrderbook(k.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(k.Name, p, assetType) +} + +// GetExchangeAccountInfo retrieves balances for all enabled currencies for the +// Kraken exchange - to-do +func (k *Kraken) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.ExchangeName = e.GetName() + response.ExchangeName = k.GetName() return response, nil } diff --git a/exchanges/lakebtc/lakebtc.go b/exchanges/lakebtc/lakebtc.go index 97121ecb..639619d2 100644 --- a/exchanges/lakebtc/lakebtc.go +++ b/exchanges/lakebtc/lakebtc.go @@ -11,6 +11,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( @@ -42,6 +43,11 @@ func (l *LakeBTC) SetDefaults() { l.Verbose = false l.Websocket = false l.RESTPollingDelay = 10 + l.RequestCurrencyPairFormat.Delimiter = "" + l.RequestCurrencyPairFormat.Uppercase = true + l.ConfigCurrencyPairFormat.Delimiter = "" + l.ConfigCurrencyPairFormat.Uppercase = true + l.AssetTypes = []string{ticker.Spot} } func (l *LakeBTC) Setup(exch config.ExchangeConfig) { @@ -57,6 +63,14 @@ func (l *LakeBTC) Setup(exch config.ExchangeConfig) { l.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") l.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") l.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := l.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = l.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } @@ -71,7 +85,7 @@ func (l *LakeBTC) GetFee(maker bool) float64 { func (l *LakeBTC) GetTicker() (map[string]LakeBTCTicker, error) { response := make(map[string]LakeBTCTickerResponse) path := fmt.Sprintf("%s/%s", LAKEBTC_API_URL, LAKEBTC_TICKER) - err := common.SendHTTPGetRequest(path, true, &response) + err := common.SendHTTPGetRequest(path, true, l.Verbose, &response) if err != nil { return nil, err } @@ -112,7 +126,7 @@ func (l *LakeBTC) GetOrderBook(currency string) (LakeBTCOrderbook, error) { } path := fmt.Sprintf("%s/%s?symbol=%s", LAKEBTC_API_URL, LAKEBTC_ORDERBOOK, common.StringToLower(currency)) resp := Response{} - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, l.Verbose, &resp) if err != nil { return LakeBTCOrderbook{}, err } @@ -151,7 +165,7 @@ func (l *LakeBTC) GetOrderBook(currency string) (LakeBTCOrderbook, error) { func (l *LakeBTC) GetTradeHistory(currency string) ([]LakeBTCTradeHistory, error) { path := fmt.Sprintf("%s/%s?symbol=%s", LAKEBTC_API_URL, LAKEBTC_TRADES, common.StringToLower(currency)) resp := []LakeBTCTradeHistory{} - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, l.Verbose, &resp) if err != nil { return nil, err } diff --git a/exchanges/lakebtc/lakebtc_wrapper.go b/exchanges/lakebtc/lakebtc_wrapper.go index a2a1f920..4066e2dc 100644 --- a/exchanges/lakebtc/lakebtc_wrapper.go +++ b/exchanges/lakebtc/lakebtc_wrapper.go @@ -3,93 +3,89 @@ package lakebtc import ( "log" "strconv" - "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +// Start starts the LakeBTC go routine func (l *LakeBTC) Start() { go l.Run() } + +// Run implements the LakeBTC wrapper func (l *LakeBTC) Run() { if l.Verbose { log.Printf("%s polling delay: %ds.\n", l.GetName(), l.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", l.GetName(), len(l.EnabledPairs), l.EnabledPairs) } - - for l.Enabled { - for _, x := range l.EnabledPairs { - currency := pair.NewCurrencyPair(x[0:3], x[3:]) - ticker, err := l.GetTickerPrice(currency) - if err != nil { - log.Println(err) - continue - } - log.Printf("LakeBTC %s: Last %f High %f Low %f Volume %f\n", exchange.FormatCurrency(currency).String(), ticker.Last, ticker.High, ticker.Low, ticker.Volume) - stats.AddExchangeInfo(l.GetName(), currency.GetFirstCurrency().String(), currency.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - } - time.Sleep(time.Second * l.RESTPollingDelay) - } } -func (l *LakeBTC) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - tickerNew, err := ticker.GetTicker(l.GetName(), p) - if err == nil { - return tickerNew, nil - } - +// UpdateTicker updates and returns the ticker for a currency pair +func (l *LakeBTC) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { tick, err := l.GetTicker() if err != nil { - return ticker.TickerPrice{}, err + return ticker.Price{}, err } - result, ok := tick[p.Pair().String()] - if !ok { - return ticker.TickerPrice{}, err + for _, x := range l.GetEnabledCurrencies() { + currency := exchange.FormatExchangeCurrency(l.Name, x).String() + var tickerPrice ticker.Price + tickerPrice.Pair = x + tickerPrice.Ask = tick[currency].Ask + tickerPrice.Bid = tick[currency].Bid + tickerPrice.Volume = tick[currency].Volume + tickerPrice.High = tick[currency].High + tickerPrice.Low = tick[currency].Low + tickerPrice.Last = tick[currency].Last + ticker.ProcessTicker(l.GetName(), x, tickerPrice, assetType) } - - var tickerPrice ticker.TickerPrice - tickerPrice.Pair = p - tickerPrice.Ask = result.Ask - tickerPrice.Bid = result.Bid - tickerPrice.Volume = result.Volume - tickerPrice.High = result.High - tickerPrice.Low = result.Low - tickerPrice.Last = result.Last - ticker.ProcessTicker(l.GetName(), p, tickerPrice) - return tickerPrice, nil + return ticker.GetTicker(l.Name, p, assetType) } -func (l *LakeBTC) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(l.GetName(), p) - if err == nil { - return ob, nil +// GetTickerPrice returns the ticker for a currency pair +func (l *LakeBTC) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(l.GetName(), p, assetType) + if err != nil { + return l.UpdateTicker(p, assetType) } + return tickerNew, nil +} - var orderBook orderbook.OrderbookBase +// GetOrderbookEx returns orderbook base on the currency pair +func (l *LakeBTC) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(l.GetName(), p, assetType) + if err == nil { + return l.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (l *LakeBTC) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base orderbookNew, err := l.GetOrderBook(p.Pair().String()) if err != nil { return orderBook, err } for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.OrderbookItem{Amount: orderbookNew.Bids[x].Amount, Price: orderbookNew.Bids[x].Price}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: orderbookNew.Bids[x].Amount, Price: orderbookNew.Bids[x].Price}) } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.OrderbookItem{Amount: orderbookNew.Asks[x].Amount, Price: orderbookNew.Asks[x].Price}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: orderbookNew.Asks[x].Amount, Price: orderbookNew.Asks[x].Price}) } - orderBook.Pair = p - orderbook.ProcessOrderbook(l.GetName(), p, orderBook) - return orderBook, nil + orderbook.ProcessOrderbook(l.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(l.Name, p, assetType) } +// GetExchangeAccountInfo retrieves balances for all enabled currencies for the +// LakeBTC exchange func (l *LakeBTC) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo response.ExchangeName = l.GetName() diff --git a/exchanges/liqui/liqui.go b/exchanges/liqui/liqui.go index 15ecdcd2..c9f8cc2f 100644 --- a/exchanges/liqui/liqui.go +++ b/exchanges/liqui/liqui.go @@ -12,32 +12,35 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( - LIQUI_API_PUBLIC_URL = "https://api.Liqui.io/api" - LIQUI_API_PRIVATE_URL = "https://api.Liqui.io/tapi" - LIQUI_API_PUBLIC_VERSION = "3" - LIQUI_API_PRIVATE_VERSION = "1" - LIQUI_INFO = "info" - LIQUI_TICKER = "ticker" - LIQUI_DEPTH = "depth" - LIQUI_TRADES = "trades" - LIQUI_ACCOUNT_INFO = "getInfo" - LIQUI_TRADE = "Trade" - LIQUI_ACTIVE_ORDERS = "ActiveOrders" - LIQUI_ORDER_INFO = "OrderInfo" - LIQUI_CANCEL_ORDER = "CancelOrder" - LIQUI_TRADE_HISTORY = "TradeHistory" - LIQUI_WITHDRAW_COIN = "WithdrawCoin" + liquiAPIPublicURL = "https://api.Liqui.io/api" + liquiAPIPrivateURL = "https://api.Liqui.io/tapi" + liquiAPIPublicVersion = "3" + liquiAPIPrivateVersion = "1" + liquiInfo = "info" + liquiTicker = "ticker" + liquiDepth = "depth" + liquiTrades = "trades" + liquiAccountInfo = "getInfo" + liquiTrade = "Trade" + liquiActiveOrders = "ActiveOrders" + liquiOrderInfo = "OrderInfo" + liquiCancelOrder = "CancelOrder" + liquiTradeHistory = "TradeHistory" + liquiWithdrawCoin = "WithdrawCoin" ) +// Liqui is the overarching type across the liqui package type Liqui struct { exchange.Base - Ticker map[string]LiquiTicker - Info LiquiInfo + Ticker map[string]Ticker + Info Info } +// SetDefaults sets current default values for liqui func (l *Liqui) SetDefaults() { l.Name = "Liqui" l.Enabled = false @@ -45,9 +48,16 @@ func (l *Liqui) SetDefaults() { l.Verbose = false l.Websocket = false l.RESTPollingDelay = 10 - l.Ticker = make(map[string]LiquiTicker) + l.Ticker = make(map[string]Ticker) + l.RequestCurrencyPairFormat.Delimiter = "_" + l.RequestCurrencyPairFormat.Uppercase = false + l.RequestCurrencyPairFormat.Separator = "-" + l.ConfigCurrencyPairFormat.Delimiter = "_" + l.ConfigCurrencyPairFormat.Uppercase = true + l.AssetTypes = []string{ticker.Spot} } +// Setup sets exchange configuration parameters for liqui func (l *Liqui) Setup(exch config.ExchangeConfig) { if !exch.Enabled { l.SetEnabled(false) @@ -61,22 +71,33 @@ func (l *Liqui) Setup(exch config.ExchangeConfig) { l.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") l.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") l.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := l.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = l.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } +// GetFee returns a fee for a specific currency func (l *Liqui) GetFee(currency string) (float64, error) { + log.Println(l.Info.Pairs) val, ok := l.Info.Pairs[common.StringToLower(currency)] if !ok { - return 0, errors.New("Currency does not exist") + return 0, errors.New("currency does not exist") } return val.Fee, nil } +// GetAvailablePairs returns all available pairs func (l *Liqui) GetAvailablePairs(nonHidden bool) []string { var pairs []string for x, y := range l.Info.Pairs { - if nonHidden && y.Hidden == 1 { + if nonHidden && y.Hidden == 1 || x == "" { continue } pairs = append(pairs, common.StringToUpper(x)) @@ -84,79 +105,77 @@ func (l *Liqui) GetAvailablePairs(nonHidden bool) []string { return pairs } -func (l *Liqui) GetInfo() (LiquiInfo, error) { - req := fmt.Sprintf("%s/%s/%s/", LIQUI_API_PUBLIC_URL, LIQUI_API_PUBLIC_VERSION, LIQUI_INFO) - resp := LiquiInfo{} - err := common.SendHTTPGetRequest(req, true, &resp) +// GetInfo provides all the information about currently active pairs, such as +// the maximum number of digits after the decimal point, the minimum price, the +// maximum price, the minimum transaction size, whether the pair is hidden, the +// commission for each pair. +func (l *Liqui) GetInfo() (Info, error) { + resp := Info{} + req := fmt.Sprintf("%s/%s/%s/", liquiAPIPublicURL, liquiAPIPublicVersion, liquiInfo) - if err != nil { - return resp, err - } - - return resp, nil + return resp, common.SendHTTPGetRequest(req, true, l.Verbose, &resp) } -func (l *Liqui) GetTicker(symbol string) (map[string]LiquiTicker, error) { +// GetTicker returns information about currently active pairs, such as: the +// maximum price, the minimum price, average price, trade volume, trade volume +// in currency, the last trade, Buy and Sell price. All information is provided +// over the past 24 hours. +// +// currencyPair - example "eth_btc" +func (l *Liqui) GetTicker(currencyPair string) (map[string]Ticker, error) { type Response struct { - Data map[string]LiquiTicker + Data map[string]Ticker } response := Response{} - req := fmt.Sprintf("%s/%s/%s/%s", LIQUI_API_PUBLIC_URL, LIQUI_API_PUBLIC_VERSION, LIQUI_TICKER, symbol) - err := common.SendHTTPGetRequest(req, true, &response.Data) + req := fmt.Sprintf("%s/%s/%s/%s", liquiAPIPublicURL, liquiAPIPublicVersion, liquiTicker, currencyPair) - if err != nil { - return nil, err - } - return response.Data, nil + return response.Data, + common.SendHTTPGetRequest(req, true, l.Verbose, &response.Data) } -func (l *Liqui) GetDepth(symbol string) (LiquiOrderbook, error) { +// GetDepth information about active orders on the pair. Additionally it accepts +// an optional GET-parameter limit, which indicates how many orders should be +// displayed (150 by default). Is set to less than 2000. +func (l *Liqui) GetDepth(currencyPair string) (Orderbook, error) { type Response struct { - Data map[string]LiquiOrderbook + Data map[string]Orderbook } response := Response{} - req := fmt.Sprintf("%s/%s/%s/%s", LIQUI_API_PUBLIC_URL, LIQUI_API_PUBLIC_VERSION, LIQUI_DEPTH, symbol) + req := fmt.Sprintf("%s/%s/%s/%s", liquiAPIPublicURL, liquiAPIPublicVersion, liquiDepth, currencyPair) - err := common.SendHTTPGetRequest(req, true, &response.Data) - if err != nil { - return LiquiOrderbook{}, err - } - - depth := response.Data[symbol] - return depth, nil + return response.Data[currencyPair], + common.SendHTTPGetRequest(req, true, l.Verbose, &response.Data) } -func (l *Liqui) GetTrades(symbol string) ([]LiquiTrades, error) { +// GetTrades returns information about the last trades. Additionally it accepts +// an optional GET-parameter limit, which indicates how many orders should be +// displayed (150 by default). The maximum allowable value is 2000. +func (l *Liqui) GetTrades(currencyPair string) ([]Trades, error) { type Response struct { - Data map[string][]LiquiTrades + Data map[string][]Trades } response := Response{} - req := fmt.Sprintf("%s/%s/%s/%s", LIQUI_API_PUBLIC_URL, LIQUI_API_PUBLIC_VERSION, LIQUI_TRADES, symbol) + req := fmt.Sprintf("%s/%s/%s/%s", liquiAPIPublicURL, liquiAPIPublicVersion, liquiTrades, currencyPair) - err := common.SendHTTPGetRequest(req, true, &response.Data) - if err != nil { - return []LiquiTrades{}, err - } - - trades := response.Data[symbol] - return trades, nil + return response.Data[currencyPair], + common.SendHTTPGetRequest(req, true, l.Verbose, &response.Data) } -func (l *Liqui) GetAccountInfo() (LiquiAccountInfo, error) { - var result LiquiAccountInfo - err := l.SendAuthenticatedHTTPRequest(LIQUI_ACCOUNT_INFO, url.Values{}, &result) +// GetAccountInfo returns information about the user’s current balance, API-key +// privileges, the number of open orders and Server Time. To use this method you +// need a privilege of the key info. +func (l *Liqui) GetAccountInfo() (AccountInfo, error) { + var result AccountInfo - if err != nil { - return result, err - } - - return result, nil + return result, + l.SendAuthenticatedHTTPRequest(liquiAccountInfo, url.Values{}, &result) } -//to-do: convert orderid to int64 +// Trade creates orders on the exchange. +// to-do: convert orderid to int64 func (l *Liqui) Trade(pair, orderType string, amount, price float64) (float64, error) { req := url.Values{} req.Add("pair", pair) @@ -164,51 +183,37 @@ func (l *Liqui) Trade(pair, orderType string, amount, price float64) (float64, e req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) req.Add("rate", strconv.FormatFloat(price, 'f', -1, 64)) - var result LiquiTrade - err := l.SendAuthenticatedHTTPRequest(LIQUI_TRADE, req, &result) + var result Trade - if err != nil { - return 0, err - } - - return result.OrderID, nil + return result.OrderID, l.SendAuthenticatedHTTPRequest(liquiTrade, req, &result) } -func (l *Liqui) GetActiveOrders(pair string) (map[string]LiquiActiveOrders, error) { +// GetActiveOrders returns the list of your active orders. +func (l *Liqui) GetActiveOrders(pair string) (map[string]ActiveOrders, error) { req := url.Values{} req.Add("pair", pair) - var result map[string]LiquiActiveOrders - err := l.SendAuthenticatedHTTPRequest(LIQUI_ACTIVE_ORDERS, req, &result) - - if err != nil { - return result, err - } - - return result, nil + var result map[string]ActiveOrders + return result, l.SendAuthenticatedHTTPRequest(liquiActiveOrders, req, &result) } -func (l *Liqui) GetOrderInfo(OrderID int64) (map[string]LiquiOrderInfo, error) { +// GetOrderInfo returns the information on particular order. +func (l *Liqui) GetOrderInfo(OrderID int64) (map[string]OrderInfo, error) { req := url.Values{} req.Add("order_id", strconv.FormatInt(OrderID, 10)) - var result map[string]LiquiOrderInfo - err := l.SendAuthenticatedHTTPRequest(LIQUI_ORDER_INFO, req, &result) - - if err != nil { - return result, err - } - - return result, nil + var result map[string]OrderInfo + return result, l.SendAuthenticatedHTTPRequest(liquiOrderInfo, req, &result) } +// CancelOrder method is used for order cancelation. func (l *Liqui) CancelOrder(OrderID int64) (bool, error) { req := url.Values{} req.Add("order_id", strconv.FormatInt(OrderID, 10)) - var result LiquiCancelOrder - err := l.SendAuthenticatedHTTPRequest(LIQUI_CANCEL_ORDER, req, &result) + var result CancelOrder + err := l.SendAuthenticatedHTTPRequest(liquiCancelOrder, req, &result) if err != nil { return false, err } @@ -216,45 +221,37 @@ func (l *Liqui) CancelOrder(OrderID int64) (bool, error) { return true, nil } -func (l *Liqui) GetTradeHistory(vals url.Values, pair string) (map[string]LiquiTradeHistory, error) { +// GetTradeHistory returns trade history +func (l *Liqui) GetTradeHistory(vals url.Values, pair string) (map[string]TradeHistory, error) { if pair != "" { vals.Add("pair", pair) } - var result map[string]LiquiTradeHistory - err := l.SendAuthenticatedHTTPRequest(LIQUI_TRADE_HISTORY, vals, &result) - - if err != nil { - return result, err - } - - return result, nil + var result map[string]TradeHistory + return result, l.SendAuthenticatedHTTPRequest(liquiTradeHistory, vals, &result) } +// WithdrawCoins is designed for cryptocurrency withdrawals. // API mentions that this isn't active now, but will be soon - you must provide the first 8 characters of the key // in your ticket to support. -func (l *Liqui) WithdrawCoins(coin string, amount float64, address string) (LiquiWithdrawCoins, error) { +func (l *Liqui) WithdrawCoins(coin string, amount float64, address string) (WithdrawCoins, error) { req := url.Values{} req.Add("coinName", coin) req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) req.Add("address", address) - var result LiquiWithdrawCoins - err := l.SendAuthenticatedHTTPRequest(LIQUI_WITHDRAW_COIN, req, &result) - - if err != nil { - return result, err - } - return result, nil + var result WithdrawCoins + return result, l.SendAuthenticatedHTTPRequest(liquiWithdrawCoin, req, &result) } +// SendAuthenticatedHTTPRequest sends an authenticated http request to liqui func (l *Liqui) SendAuthenticatedHTTPRequest(method string, values url.Values, result interface{}) (err error) { if !l.AuthenticatedAPISupport { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, l.Name) } if l.Nonce.Get() == 0 { - l.Nonce.Set(time.Now().UnixNano()) + l.Nonce.Set(time.Now().Unix()) } else { l.Nonce.Inc() } @@ -265,7 +262,7 @@ func (l *Liqui) SendAuthenticatedHTTPRequest(method string, values url.Values, r hmac := common.GetHMAC(common.HashSHA512, []byte(encoded), []byte(l.APISecret)) if l.Verbose { - log.Printf("Sending POST request to %s calling method %s with params %s\n", LIQUI_API_PRIVATE_URL, method, encoded) + log.Printf("Sending POST request to %s calling method %s with params %s\n", liquiAPIPrivateURL, method, encoded) } headers := make(map[string]string) @@ -273,15 +270,14 @@ func (l *Liqui) SendAuthenticatedHTTPRequest(method string, values url.Values, r headers["Sign"] = common.HexEncodeToString(hmac) headers["Content-Type"] = "application/x-www-form-urlencoded" - resp, err := common.SendHTTPRequest("POST", LIQUI_API_PRIVATE_URL, headers, strings.NewReader(encoded)) - + resp, err := common.SendHTTPRequest("POST", liquiAPIPrivateURL, headers, strings.NewReader(encoded)) if err != nil { return err } - response := LiquiResponse{} - err = common.JSONDecode([]byte(resp), &response) + response := Response{} + err = common.JSONDecode([]byte(resp), &response) if err != nil { return err } @@ -291,15 +287,14 @@ func (l *Liqui) SendAuthenticatedHTTPRequest(method string, values url.Values, r } jsonEncoded, err := common.JSONEncode(response.Return) - if err != nil { return err } err = common.JSONDecode(jsonEncoded, &result) - if err != nil { return err } + return nil } diff --git a/exchanges/liqui/liqui_test.go b/exchanges/liqui/liqui_test.go new file mode 100644 index 00000000..eaddeb06 --- /dev/null +++ b/exchanges/liqui/liqui_test.go @@ -0,0 +1,125 @@ +package liqui + +import ( + "net/url" + "testing" + + "github.com/thrasher-/gocryptotrader/config" +) + +var l Liqui + +const ( + apiKey = "" + apiSecret = "" +) + +func TestSetDefaults(t *testing.T) { + l.SetDefaults() +} + +func TestSetup(t *testing.T) { + cfg := config.GetConfig() + cfg.LoadConfig("../../testdata/configtest.dat") + liquiConfig, err := cfg.GetExchangeConfig("Liqui") + if err != nil { + t.Error("Test Failed - liqui Setup() init error") + } + + liquiConfig.AuthenticatedAPISupport = true + liquiConfig.APIKey = apiKey + liquiConfig.APISecret = apiSecret + + l.Setup(liquiConfig) +} + +func TestGetFee(t *testing.T) { + _, err := l.GetFee("usd") + if err == nil { + t.Error("Test Failed - liqui GetFee() error", err) + } +} + +func TestGetAvailablePairs(t *testing.T) { + v := l.GetAvailablePairs(false) + if len(v) != 0 { + t.Error("Test Failed - liqui GetFee() error") + } +} + +func TestGetInfo(t *testing.T) { + _, err := l.GetInfo() + if err != nil { + t.Error("Test Failed - liqui GetInfo() error", err) + } +} + +func TestGetTicker(t *testing.T) { + _, err := l.GetTicker("eth_btc") + if err != nil { + t.Error("Test Failed - liqui GetTicker() error", err) + } +} + +func TestGetDepth(t *testing.T) { + _, err := l.GetDepth("eth_btc") + if err != nil { + t.Error("Test Failed - liqui GetDepth() error", err) + } +} + +func TestGetTrades(t *testing.T) { + _, err := l.GetTrades("eth_btc") + if err != nil { + t.Error("Test Failed - liqui GetTrades() error", err) + } +} + +func TestGetAccountInfo(t *testing.T) { + _, err := l.GetAccountInfo() + if err == nil { + t.Error("Test Failed - liqui GetAccountInfo() error", err) + } +} + +func TestTrade(t *testing.T) { + _, err := l.Trade("", "", 0, 1) + if err == nil { + t.Error("Test Failed - liqui Trade() error", err) + } +} + +func TestGetActiveOrders(t *testing.T) { + _, err := l.GetActiveOrders("eth_btc") + if err == nil { + t.Error("Test Failed - liqui GetActiveOrders() error", err) + } +} + +func TestGetOrderInfo(t *testing.T) { + _, err := l.GetOrderInfo(1337) + if err == nil { + t.Error("Test Failed - liqui GetOrderInfo() error", err) + } +} + +func TestCancelOrder(t *testing.T) { + _, err := l.CancelOrder(1337) + if err == nil { + t.Error("Test Failed - liqui CancelOrder() error", err) + } +} + +func TestGetTradeHistory(t *testing.T) { + _, err := l.GetTradeHistory(url.Values{}, "") + if err == nil { + t.Error("Test Failed - liqui GetTradeHistory() error", err) + } +} + +func TestWithdrawCoins(t *testing.T) { + _, err := l.WithdrawCoins("btc", 1337, "someaddr") + if err == nil { + t.Error("Test Failed - liqui WithdrawCoins() error", err) + } +} diff --git a/exchanges/liqui/liqui_types.go b/exchanges/liqui/liqui_types.go index bf25573d..52a8167d 100644 --- a/exchanges/liqui/liqui_types.go +++ b/exchanges/liqui/liqui_types.go @@ -1,6 +1,23 @@ package liqui -type LiquiTicker struct { +// Info holds the current pair information as well as server time +type Info struct { + ServerTime int64 `json:"server_time"` + Pairs map[string]PairData `json:"pairs"` +} + +// PairData is a sub-type for Info +type PairData struct { + DecimalPlaces int `json:"decimal_places"` + MinPrice float64 `json:"min_price"` + MaxPrice float64 `json:"max_price"` + MinAmount float64 `json:"min_amount"` + Hidden int `json:"hidden"` + Fee float64 `json:"fee"` +} + +// Ticker contains ticker information +type Ticker struct { High float64 Low float64 Avg float64 @@ -12,12 +29,14 @@ type LiquiTicker struct { Updated int64 } -type LiquiOrderbook struct { +// Orderbook references both ask and bid sides +type Orderbook struct { Asks [][]float64 `json:"asks"` Bids [][]float64 `json:"bids"` } -type LiquiTrades struct { +// Trades contains trade information +type Trades struct { Type string `json:"type"` Price float64 `json:"bid"` Amount float64 `json:"amount"` @@ -25,27 +44,8 @@ type LiquiTrades struct { Timestamp int64 `json:"timestamp"` } -type LiquiResponse struct { - Return interface{} `json:"return"` - Success int `json:"success"` - Error string `json:"error"` -} - -type LiquiPair struct { - DecimalPlaces int `json:"decimal_places"` - MinPrice float64 `json:"min_price"` - MaxPrice float64 `json:"max_price"` - MinAmount float64 `json:"min_amount"` - Hidden int `json:"hidden"` - Fee float64 `json:"fee"` -} - -type LiquiInfo struct { - ServerTime int64 `json:"server_time"` - Pairs map[string]LiquiPair `json:"pairs"` -} - -type LiquiAccountInfo struct { +// AccountInfo contains full account details information +type AccountInfo struct { Funds map[string]float64 `json:"funds"` Rights struct { Info bool `json:"info"` @@ -57,14 +57,8 @@ type LiquiAccountInfo struct { OpenOrders int `json:"open_orders"` } -type LiquiTrade struct { - Received float64 `json:"received"` - Remains float64 `json:"remains"` - OrderID float64 `json:"order_id"` - Funds map[string]float64 `json:"funds"` -} - -type LiquiActiveOrders struct { +// ActiveOrders holds active order information +type ActiveOrders struct { Pair string `json:"pair"` Type string `json:"sell"` Amount float64 `json:"amount"` @@ -73,7 +67,8 @@ type LiquiActiveOrders struct { Status int `json:"status"` } -type LiquiOrderInfo struct { +// OrderInfo holds specific order information +type OrderInfo struct { Pair string `json:"pair"` Type string `json:"sell"` StartAmount float64 `json:"start_amount"` @@ -83,12 +78,22 @@ type LiquiOrderInfo struct { Status int `json:"status"` } -type LiquiCancelOrder struct { +// CancelOrder holds cancelled order information +type CancelOrder struct { OrderID float64 `json:"order_id"` Funds map[string]float64 `json:"funds"` } -type LiquiTradeHistory struct { +// Trade holds trading information +type Trade struct { + Received float64 `json:"received"` + Remains float64 `json:"remains"` + OrderID float64 `json:"order_id"` + Funds map[string]float64 `json:"funds"` +} + +// TradeHistory contains trade history data +type TradeHistory struct { Pair string `json:"pair"` Type string `json:"type"` Amount float64 `json:"amount"` @@ -98,7 +103,15 @@ type LiquiTradeHistory struct { Timestamp float64 `json:"timestamp"` } -type LiquiWithdrawCoins struct { +// Response is a generalized return type +type Response struct { + Return interface{} `json:"return"` + Success int `json:"success"` + Error string `json:"error"` +} + +// WithdrawCoins shows the amount of coins withdrawn from liqui not yet available +type WithdrawCoins struct { TID int64 `json:"tId"` AmountSent float64 `json:"amountSent"` Funds map[string]float64 `json:"funds"` diff --git a/exchanges/liqui/liqui_wrapper.go b/exchanges/liqui/liqui_wrapper.go index 634fc1f4..8e22e8ba 100644 --- a/exchanges/liqui/liqui_wrapper.go +++ b/exchanges/liqui/liqui_wrapper.go @@ -1,22 +1,21 @@ package liqui import ( - "errors" "log" - "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +// Start starts the Liqui go routine func (l *Liqui) Start() { go l.Run() } +// Run implements the Liqui wrapper func (l *Liqui) Run() { if l.Verbose { log.Printf("%s polling delay: %ds.\n", l.GetName(), l.RESTPollingDelay) @@ -29,86 +28,89 @@ func (l *Liqui) Run() { log.Printf("%s Unable to fetch info.\n", l.GetName()) } else { exchangeProducts := l.GetAvailablePairs(true) - err = l.UpdateAvailableCurrencies(exchangeProducts) + err = l.UpdateAvailableCurrencies(exchangeProducts, false) if err != nil { log.Printf("%s Failed to get config.\n", l.GetName()) } } - - pairs := []string{} - for _, x := range l.EnabledPairs { - currencies := common.SplitStrings(x, "_") - x = common.StringToLower(currencies[0]) + "_" + common.StringToLower(currencies[1]) - pairs = append(pairs, x) - } - pairsString := common.JoinStrings(pairs, "-") - - for l.Enabled { - go func() { - ticker, err := l.GetTicker(pairsString) - if err != nil { - log.Println(err) - return - } - for x, y := range ticker { - currency := pair.NewCurrencyPairDelimiter(common.StringToUpper(x), "_") - log.Printf("Liqui %s: Last %f High %f Low %f Volume %f\n", exchange.FormatCurrency(currency).String(), y.Last, y.High, y.Low, y.Vol_cur) - l.Ticker[x] = y - stats.AddExchangeInfo(l.GetName(), currency.GetFirstCurrency().String(), currency.GetSecondCurrency().String(), y.Last, y.Vol_cur) - } - }() - time.Sleep(time.Second * l.RESTPollingDelay) - } } -func (l *Liqui) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - var tickerPrice ticker.TickerPrice - tick, ok := l.Ticker[p.Pair().Lower().String()] - if !ok { - return tickerPrice, errors.New("Unable to get currency.") +// UpdateTicker updates and returns the ticker for a currency pair +func (l *Liqui) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price + pairsString, err := exchange.GetAndFormatExchangeCurrencies(l.Name, + l.GetEnabledCurrencies()) + if err != nil { + return tickerPrice, err } - tickerPrice.Pair = p - tickerPrice.Ask = tick.Buy - tickerPrice.Bid = tick.Sell - tickerPrice.Low = tick.Low - tickerPrice.Last = tick.Last - tickerPrice.Volume = tick.Vol_cur - tickerPrice.High = tick.High - ticker.ProcessTicker(l.GetName(), p, tickerPrice) - return tickerPrice, nil + + result, err := l.GetTicker(pairsString.String()) + if err != nil { + return tickerPrice, err + } + + for _, x := range l.GetEnabledCurrencies() { + currency := exchange.FormatExchangeCurrency(l.Name, x).String() + var tp ticker.Price + tp.Pair = x + tp.Last = result[currency].Last + tp.Ask = result[currency].Sell + tp.Bid = result[currency].Buy + tp.Last = result[currency].Last + tp.Low = result[currency].Low + tp.Volume = result[currency].Vol_cur + ticker.ProcessTicker(l.Name, x, tp, assetType) + } + + return ticker.GetTicker(l.Name, p, assetType) } -func (l *Liqui) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(l.GetName(), p) +// GetTickerPrice returns the ticker for a currency pair +func (l *Liqui) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(l.Name, p, assetType) + if err != nil { + return l.UpdateTicker(p, assetType) + } + return tickerNew, nil +} + +// GetOrderbookEx returns orderbook base on the currency pair +func (l *Liqui) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(l.Name, p, assetType) if err == nil { - return ob, nil + return l.UpdateOrderbook(p, assetType) } + return ob, nil +} - var orderBook orderbook.OrderbookBase - orderbookNew, err := l.GetDepth(p.Pair().Lower().String()) +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (l *Liqui) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + orderbookNew, err := l.GetDepth(exchange.FormatExchangeCurrency(l.Name, p).String()) if err != nil { return orderBook, err } for x := range orderbookNew.Bids { data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.OrderbookItem{Amount: data[1], Price: data[0]}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data[1], Price: data[0]}) } for x := range orderbookNew.Asks { data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.OrderbookItem{Amount: data[1], Price: data[0]}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data[1], Price: data[0]}) } - orderBook.Pair = p - orderbook.ProcessOrderbook(l.GetName(), p, orderBook) - return orderBook, nil + + orderbook.ProcessOrderbook(l.Name, p, orderBook, assetType) + return orderbook.GetOrderbook(l.Name, p, assetType) } -//GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the Liqui exchange -func (e *Liqui) GetExchangeAccountInfo() (exchange.AccountInfo, error) { +// GetExchangeAccountInfo retrieves balances for all enabled currencies for the +// Liqui exchange +func (l *Liqui) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.ExchangeName = e.GetName() - accountBalance, err := e.GetAccountInfo() + response.ExchangeName = l.GetName() + accountBalance, err := l.GetAccountInfo() if err != nil { return response, err } diff --git a/exchanges/localbitcoins/localbitcoins.go b/exchanges/localbitcoins/localbitcoins.go index d657869e..e07bbe1f 100644 --- a/exchanges/localbitcoins/localbitcoins.go +++ b/exchanges/localbitcoins/localbitcoins.go @@ -12,6 +12,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( @@ -38,6 +39,11 @@ func (l *LocalBitcoins) SetDefaults() { l.Verbose = false l.Websocket = false l.RESTPollingDelay = 10 + l.RequestCurrencyPairFormat.Delimiter = "" + l.RequestCurrencyPairFormat.Uppercase = true + l.ConfigCurrencyPairFormat.Delimiter = "" + l.ConfigCurrencyPairFormat.Uppercase = true + l.AssetTypes = []string{ticker.Spot} } func (l *LocalBitcoins) Setup(exch config.ExchangeConfig) { @@ -53,6 +59,14 @@ func (l *LocalBitcoins) Setup(exch config.ExchangeConfig) { l.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") l.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") l.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := l.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = l.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } @@ -66,7 +80,7 @@ func (l *LocalBitcoins) GetFee(maker bool) float64 { func (l *LocalBitcoins) GetTicker() (map[string]LocalBitcoinsTicker, error) { result := make(map[string]LocalBitcoinsTicker) - err := common.SendHTTPGetRequest(LOCALBITCOINS_API_URL+LOCALBITCOINS_API_TICKER, true, &result) + err := common.SendHTTPGetRequest(LOCALBITCOINS_API_URL+LOCALBITCOINS_API_TICKER, true, l.Verbose, &result) if err != nil { return result, err @@ -78,7 +92,7 @@ func (l *LocalBitcoins) GetTicker() (map[string]LocalBitcoinsTicker, error) { func (l *LocalBitcoins) GetTrades(currency string, values url.Values) ([]LocalBitcoinsTrade, error) { path := common.EncodeURLValues(fmt.Sprintf("%s/%s/trades.json", LOCALBITCOINS_API_URL+LOCALBITCOINS_API_BITCOINCHARTS, currency), values) result := []LocalBitcoinsTrade{} - err := common.SendHTTPGetRequest(path, true, &result) + err := common.SendHTTPGetRequest(path, true, l.Verbose, &result) if err != nil { return result, err @@ -95,7 +109,7 @@ func (l *LocalBitcoins) GetOrderbook(currency string) (LocalBitcoinsOrderbook, e path := fmt.Sprintf("%s/%s/orderbook.json", LOCALBITCOINS_API_URL+LOCALBITCOINS_API_BITCOINCHARTS, currency) resp := response{} - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, l.Verbose, &resp) if err != nil { return LocalBitcoinsOrderbook{}, err @@ -148,7 +162,7 @@ func (l *LocalBitcoins) GetAccountInfo(username string, self bool) (LocalBitcoin } } else { path := fmt.Sprintf("%s/api/account_info/%s/", LOCALBITCOINS_API_URL, username) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, l.Verbose, &resp) if err != nil { return resp.Data, err diff --git a/exchanges/localbitcoins/localbitcoins_wrapper.go b/exchanges/localbitcoins/localbitcoins_wrapper.go index a4642ba2..9cfc99e9 100644 --- a/exchanges/localbitcoins/localbitcoins_wrapper.go +++ b/exchanges/localbitcoins/localbitcoins_wrapper.go @@ -2,73 +2,92 @@ package localbitcoins import ( "log" - "time" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +// Start starts the LocalBitcoins go routine func (l *LocalBitcoins) Start() { go l.Run() } +// Run implements the LocalBitcoins wrapper func (l *LocalBitcoins) Run() { if l.Verbose { log.Printf("%s polling delay: %ds.\n", l.GetName(), l.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", l.GetName(), len(l.EnabledPairs), l.EnabledPairs) } - - for l.Enabled { - for _, x := range l.EnabledPairs { - currency := pair.NewCurrencyPair("BTC", x[3:]) - ticker, err := l.GetTickerPrice(currency) - - if err != nil { - log.Println(err) - return - } - - log.Printf("LocalBitcoins BTC %s: Last %f Volume %f\n", exchange.FormatCurrency(currency).String(), ticker.Last, ticker.Volume) - stats.AddExchangeInfo(l.GetName(), currency.GetFirstCurrency().String(), currency.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - } - time.Sleep(time.Second * l.RESTPollingDelay) - } } -func (l *LocalBitcoins) GetTickerPrice(p pair.CurrencyPair) (ticker.TickerPrice, error) { - tickerNew, err := ticker.GetTicker(l.GetName(), p) - if err == nil { - return tickerNew, nil - } - +// UpdateTicker updates and returns the ticker for a currency pair +func (l *LocalBitcoins) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price tick, err := l.GetTicker() if err != nil { - return ticker.TickerPrice{}, err + return tickerPrice, err } - var tickerPrice ticker.TickerPrice - for key, value := range tick { - tickerPrice.Pair = p - tickerPrice.Last = value.Rates.Last - tickerPrice.Pair.SecondCurrency = pair.CurrencyItem(key) - tickerPrice.Volume = value.VolumeBTC - ticker.ProcessTicker(l.GetName(), p, tickerPrice) + for _, x := range l.GetEnabledCurrencies() { + currency := x.SecondCurrency.String() + var tp ticker.Price + tp.Pair = x + tp.Last = tick[currency].Rates.Last + tp.Volume = tick[currency].VolumeBTC + ticker.ProcessTicker(l.GetName(), x, tp, assetType) } - return tickerPrice, nil + + return ticker.GetTicker(l.GetName(), p, assetType) } -func (l *LocalBitcoins) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, error) { - return orderbook.OrderbookBase{}, nil +// GetTickerPrice returns the ticker for a currency pair +func (l *LocalBitcoins) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(l.GetName(), p, assetType) + if err == nil { + return l.UpdateTicker(p, assetType) + } + return tickerNew, nil } -//GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the LocalBitcoins exchange -func (e *LocalBitcoins) GetExchangeAccountInfo() (exchange.AccountInfo, error) { +// GetOrderbookEx returns orderbook base on the currency pair +func (l *LocalBitcoins) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(l.GetName(), p, assetType) + if err == nil { + return l.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (l *LocalBitcoins) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + orderbookNew, err := l.GetOrderbook(p.GetSecondCurrency().String()) + if err != nil { + return orderBook, err + } + + for x := range orderbookNew.Bids { + data := orderbookNew.Bids[x] + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data.Amount, Price: data.Price}) + } + + for x := range orderbookNew.Asks { + data := orderbookNew.Asks[x] + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data.Amount, Price: data.Price}) + } + + orderbook.ProcessOrderbook(l.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(l.Name, p, assetType) +} + +// GetExchangeAccountInfo retrieves balances for all enabled currencies for the +// LocalBitcoins exchange +func (l *LocalBitcoins) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.ExchangeName = e.GetName() - accountBalance, err := e.GetWalletBalance() + response.ExchangeName = l.GetName() + accountBalance, err := l.GetWalletBalance() if err != nil { return response, err } diff --git a/exchanges/nonce/nonce.go b/exchanges/nonce/nonce.go index 79ff8751..8f87529d 100644 --- a/exchanges/nonce/nonce.go +++ b/exchanges/nonce/nonce.go @@ -3,12 +3,17 @@ package nonce import ( "strconv" "sync" + "time" ) // Nonce struct holds the nonce value type Nonce struct { + // Standard nonce n int64 mtx sync.Mutex + // Hash table exclusive exchange specific nonce values + boundedCall map[string]int64 + boundedMtx sync.Mutex } // Inc increments the nonce value @@ -47,3 +52,33 @@ func (n *Nonce) String() string { n.mtx.Unlock() return result } + +// Value is a return type for GetValue +type Value int64 + +// GetValue returns a nonce value and can be set as a higher precision. Values +// stored in an exchange specific hash table using a single locked call. +func (n *Nonce) GetValue(exchName string, nanoPrecision bool) Value { + n.boundedMtx.Lock() + defer n.boundedMtx.Unlock() + + if n.boundedCall == nil { + n.boundedCall = make(map[string]int64) + } + + if n.boundedCall[exchName] == 0 { + if nanoPrecision { + n.boundedCall[exchName] = time.Now().UnixNano() + return Value(n.boundedCall[exchName]) + } + n.boundedCall[exchName] = time.Now().Unix() + return Value(n.boundedCall[exchName]) + } + n.boundedCall[exchName]++ + return Value(n.boundedCall[exchName]) +} + +// String is a Value method that changes format to a string +func (v Value) String() string { + return strconv.FormatInt(int64(v), 10) +} diff --git a/exchanges/nonce/nonce_test.go b/exchanges/nonce/nonce_test.go index 8bf6d6da..74996d2c 100644 --- a/exchanges/nonce/nonce_test.go +++ b/exchanges/nonce/nonce_test.go @@ -1,6 +1,7 @@ package nonce import ( + "strconv" "testing" "time" ) @@ -56,6 +57,16 @@ func TestString(t *testing.T) { } } +func TestGetValue(t *testing.T) { + var nonce Nonce + timeNowNano := strconv.FormatInt(time.Now().UnixNano(), 10) + nValue := nonce.GetValue("dingdong", true).String() + + if timeNowNano == nValue { + t.Error("Test failed - GetValue() error, incorrect values") + } +} + func TestNonceConcurrency(t *testing.T) { var nonce Nonce nonce.Set(12312) diff --git a/exchanges/okcoin/okcoin.go b/exchanges/okcoin/okcoin.go index 0f2994aa..12466207 100644 --- a/exchanges/okcoin/okcoin.go +++ b/exchanges/okcoin/okcoin.go @@ -12,6 +12,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( @@ -78,6 +79,13 @@ type OKCoin struct { WebsocketConn *websocket.Conn } +func (o *OKCoin) setCurrencyPairFormats() { + o.RequestCurrencyPairFormat.Delimiter = "_" + o.RequestCurrencyPairFormat.Uppercase = false + o.ConfigCurrencyPairFormat.Delimiter = "" + o.ConfigCurrencyPairFormat.Uppercase = true +} + func (o *OKCoin) SetDefaults() { o.SetErrorDefaults() o.SetWebsocketErrorDefaults() @@ -86,16 +94,20 @@ func (o *OKCoin) SetDefaults() { o.Websocket = false o.RESTPollingDelay = 10 o.FuturesValues = []string{"this_week", "next_week", "quarter"} + o.AssetTypes = []string{ticker.Spot} if !okcoinDefaultsSet { + o.AssetTypes = append(o.AssetTypes, o.FuturesValues...) o.APIUrl = OKCOIN_API_URL o.Name = "OKCOIN International" o.WebsocketURL = OKCOIN_WEBSOCKET_URL okcoinDefaultsSet = true + o.setCurrencyPairFormats() } else { o.APIUrl = OKCOIN_API_URL_CHINA o.Name = "OKCOIN China" o.WebsocketURL = OKCOIN_WEBSOCKET_URL_CHINA + o.setCurrencyPairFormats() } } @@ -112,6 +124,14 @@ func (o *OKCoin) Setup(exch config.ExchangeConfig) { o.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") o.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") o.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := o.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = o.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } @@ -132,7 +152,7 @@ func (o *OKCoin) GetTicker(symbol string) (OKCoinTicker, error) { vals := url.Values{} vals.Set("symbol", symbol) path := common.EncodeURLValues(o.APIUrl+OKCOIN_TICKER, vals) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, o.Verbose, &resp) if err != nil { return OKCoinTicker{}, err } @@ -151,7 +171,7 @@ func (o *OKCoin) GetOrderBook(symbol string, size int64, merge bool) (OKCoinOrde } path := common.EncodeURLValues(o.APIUrl+OKCOIN_DEPTH, vals) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, o.Verbose, &resp) if err != nil { return resp, err } @@ -167,7 +187,7 @@ func (o *OKCoin) GetTrades(symbol string, since int64) ([]OKCoinTrades, error) { } path := common.EncodeURLValues(o.APIUrl+OKCOIN_TRADES, vals) - err := common.SendHTTPGetRequest(path, true, &result) + err := common.SendHTTPGetRequest(path, true, o.Verbose, &result) if err != nil { return nil, err } @@ -189,7 +209,7 @@ func (o *OKCoin) GetKline(symbol, klineType string, size, since int64) ([]interf } path := common.EncodeURLValues(o.APIUrl+OKCOIN_KLINE, vals) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, o.Verbose, &resp) if err != nil { return nil, err } @@ -203,7 +223,7 @@ func (o *OKCoin) GetFuturesTicker(symbol, contractType string) (OKCoinFuturesTic vals.Set("symbol", symbol) vals.Set("contract_type", contractType) path := common.EncodeURLValues(o.APIUrl+OKCOIN_FUTURES_TICKER, vals) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, o.Verbose, &resp) if err != nil { return OKCoinFuturesTicker{}, err } @@ -224,7 +244,7 @@ func (o *OKCoin) GetFuturesDepth(symbol, contractType string, size int64, merge } path := common.EncodeURLValues(o.APIUrl+OKCOIN_FUTURES_DEPTH, vals) - err := common.SendHTTPGetRequest(path, true, &result) + err := common.SendHTTPGetRequest(path, true, o.Verbose, &result) if err != nil { return result, err } @@ -238,7 +258,7 @@ func (o *OKCoin) GetFuturesTrades(symbol, contractType string) ([]OKCoinFuturesT vals.Set("contract_type", contractType) path := common.EncodeURLValues(o.APIUrl+OKCOIN_FUTURES_TRADES, vals) - err := common.SendHTTPGetRequest(path, true, &result) + err := common.SendHTTPGetRequest(path, true, o.Verbose, &result) if err != nil { return nil, err } @@ -255,7 +275,7 @@ func (o *OKCoin) GetFuturesIndex(symbol string) (float64, error) { vals.Set("symbol", symbol) path := common.EncodeURLValues(o.APIUrl+OKCOIN_FUTURES_INDEX, vals) - err := common.SendHTTPGetRequest(path, true, &result) + err := common.SendHTTPGetRequest(path, true, o.Verbose, &result) if err != nil { return 0, err } @@ -268,7 +288,7 @@ func (o *OKCoin) GetFuturesExchangeRate() (float64, error) { } result := Response{} - err := common.SendHTTPGetRequest(o.APIUrl+OKCOIN_EXCHANGE_RATE, true, &result) + err := common.SendHTTPGetRequest(o.APIUrl+OKCOIN_EXCHANGE_RATE, true, o.Verbose, &result) if err != nil { return result.Rate, err } @@ -284,7 +304,7 @@ func (o *OKCoin) GetFuturesEstimatedPrice(symbol string) (float64, error) { vals := url.Values{} vals.Set("symbol", symbol) path := common.EncodeURLValues(o.APIUrl+OKCOIN_FUTURES_ESTIMATED_PRICE, vals) - err := common.SendHTTPGetRequest(path, true, &result) + err := common.SendHTTPGetRequest(path, true, o.Verbose, &result) if err != nil { return result.Price, err } @@ -306,7 +326,7 @@ func (o *OKCoin) GetFuturesKline(symbol, klineType, contractType string, size, s } path := common.EncodeURLValues(o.APIUrl+OKCOIN_FUTURES_KLINE, vals) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, o.Verbose, &resp) if err != nil { return nil, err @@ -321,7 +341,7 @@ func (o *OKCoin) GetFuturesHoldAmount(symbol, contractType string) ([]OKCoinFutu vals.Set("contract_type", contractType) path := common.EncodeURLValues(o.APIUrl+OKCOIN_FUTURES_HOLD_AMOUNT, vals) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, o.Verbose, &resp) if err != nil { return nil, err @@ -342,7 +362,7 @@ func (o *OKCoin) GetFuturesExplosive(symbol, contractType string, status, curren vals.Set("page_length", strconv.FormatInt(pageLength, 10)) path := common.EncodeURLValues(o.APIUrl+OKCOIN_FUTURES_EXPLOSIVE, vals) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, o.Verbose, &resp) if err != nil { return nil, err diff --git a/exchanges/okcoin/okcoin_wrapper.go b/exchanges/okcoin/okcoin_wrapper.go index 7e869fa6..9b155bfc 100644 --- a/exchanges/okcoin/okcoin_wrapper.go +++ b/exchanges/okcoin/okcoin_wrapper.go @@ -2,21 +2,20 @@ package okcoin import ( "log" - "time" "github.com/thrasher-/gocryptotrader/common" - "github.com/thrasher-/gocryptotrader/currency" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +// Start starts the OKCoin go routine func (o *OKCoin) Start() { go o.Run() } +// Run implements the OKCoin wrapper func (o *OKCoin) Run() { if o.Verbose { log.Printf("%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket), o.WebsocketURL) @@ -27,105 +26,90 @@ func (o *OKCoin) Run() { if o.Websocket { go o.WebsocketClient() } +} - for o.Enabled { - for _, x := range o.EnabledPairs { - curr := pair.NewCurrencyPair(x[0:3], x[3:]) - curr.Delimiter = "_" - if o.APIUrl == OKCOIN_API_URL { - for _, y := range o.FuturesValues { - futuresValue := y - go func() { - ticker, err := o.GetFuturesTicker(curr.Pair().Lower().String(), futuresValue) - if err != nil { - log.Println(err) - return - } - log.Printf("OKCoin Intl Futures %s (%s): Last %f High %f Low %f Volume %f\n", exchange.FormatCurrency(curr).String(), futuresValue, ticker.Last, ticker.High, ticker.Low, ticker.Vol) - stats.AddExchangeInfo(o.GetName(), curr.GetFirstCurrency().String(), curr.GetSecondCurrency().String(), ticker.Last, ticker.Vol) - }() - } - go func() { - ticker, err := o.GetTickerPrice(curr) - if err != nil { - log.Println(err) - return - } - log.Printf("OKCoin Intl Spot %s: Last %f High %f Low %f Volume %f\n", exchange.FormatCurrency(curr).String(), ticker.Last, ticker.High, ticker.Low, ticker.Volume) - stats.AddExchangeInfo(o.GetName(), curr.GetFirstCurrency().String(), curr.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - }() - } else { - go func() { - ticker, err := o.GetTickerPrice(curr) - if err != nil { - log.Println(err) - return - } - tickerLastUSD, _ := currency.ConvertCurrency(ticker.Last, "CNY", "USD") - tickerHighUSD, _ := currency.ConvertCurrency(ticker.High, "CNY", "USD") - tickerLowUSD, _ := currency.ConvertCurrency(ticker.Low, "CNY", "USD") - log.Printf("OKCoin China %s: Last %f (%f) High %f (%f) Low %f (%f) Volume %f\n", exchange.FormatCurrency(curr).String(), tickerLastUSD, ticker.Last, tickerHighUSD, ticker.High, tickerLowUSD, ticker.Low, ticker.Volume) - stats.AddExchangeInfo(o.GetName(), curr.GetFirstCurrency().String(), curr.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - stats.AddExchangeInfo(o.GetName(), curr.GetFirstCurrency().String(), "USD", tickerLastUSD, ticker.Volume) - }() - } +// UpdateTicker updates and returns the ticker for a currency pair +func (o *OKCoin) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + currency := exchange.FormatExchangeCurrency(o.Name, p).String() + var tickerPrice ticker.Price + + if assetType != ticker.Spot && o.APIUrl == OKCOIN_API_URL { + tick, err := o.GetFuturesTicker(currency, assetType) + if err != nil { + return tickerPrice, err } - time.Sleep(time.Second * o.RESTPollingDelay) + tickerPrice.Pair = p + tickerPrice.Ask = tick.Sell + tickerPrice.Bid = tick.Buy + tickerPrice.Low = tick.Low + tickerPrice.Last = tick.Last + tickerPrice.Volume = tick.Vol + tickerPrice.High = tick.High + ticker.ProcessTicker(o.GetName(), p, tickerPrice, assetType) + } else { + tick, err := o.GetTicker(currency) + if err != nil { + return tickerPrice, err + } + tickerPrice.Pair = p + tickerPrice.Ask = tick.Sell + tickerPrice.Bid = tick.Buy + tickerPrice.Low = tick.Low + tickerPrice.Last = tick.Last + tickerPrice.Volume = tick.Vol + tickerPrice.High = tick.High + ticker.ProcessTicker(o.GetName(), p, tickerPrice, ticker.Spot) + } + return ticker.GetTicker(o.Name, p, assetType) } -func (o *OKCoin) GetTickerPrice(currency pair.CurrencyPair) (ticker.TickerPrice, error) { - tickerNew, err := ticker.GetTicker(o.GetName(), currency) - if err == nil { - return tickerNew, nil - } - - var tickerPrice ticker.TickerPrice - tick, err := o.GetTicker(currency.Pair().Lower().String()) +// GetTickerPrice returns the ticker for a currency pair +func (o *OKCoin) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(o.GetName(), p, assetType) if err != nil { - return tickerPrice, err + return o.UpdateTicker(p, assetType) } - tickerPrice.Pair = currency - tickerPrice.Ask = tick.Sell - tickerPrice.Bid = tick.Buy - tickerPrice.Low = tick.Low - tickerPrice.Last = tick.Last - tickerPrice.Volume = tick.Vol - tickerPrice.High = tick.High - ticker.ProcessTicker(o.GetName(), currency, tickerPrice) - return tickerPrice, nil + return tickerNew, nil } -func (o *OKCoin) GetOrderbookEx(currency pair.CurrencyPair) (orderbook.OrderbookBase, error) { - ob, err := orderbook.GetOrderbook(o.GetName(), currency) +// GetOrderbookEx returns orderbook base on the currency pair +func (o *OKCoin) GetOrderbookEx(currency pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(o.GetName(), currency, assetType) if err == nil { - return ob, nil + return o.UpdateOrderbook(currency, assetType) } + return ob, nil +} - var orderBook orderbook.OrderbookBase - orderbookNew, err := o.GetOrderBook(currency.Pair().Lower().String(), 200, false) +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (o *OKCoin) UpdateOrderbook(currency pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + orderbookNew, err := o.GetOrderBook(exchange.FormatExchangeCurrency(o.Name, currency).String(), 200, false) if err != nil { return orderBook, err } for x := range orderbookNew.Bids { data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.OrderbookItem{Amount: data[1], Price: data[0]}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data[1], Price: data[0]}) } for x := range orderbookNew.Asks { data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.OrderbookItem{Amount: data[1], Price: data[0]}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data[1], Price: data[0]}) } - orderBook.Pair = currency - orderbook.ProcessOrderbook(o.GetName(), currency, orderBook) - return orderBook, nil + + orderbook.ProcessOrderbook(o.GetName(), currency, orderBook, assetType) + return orderbook.GetOrderbook(o.Name, currency, assetType) } -func (e *OKCoin) GetExchangeAccountInfo() (exchange.AccountInfo, error) { +// GetExchangeAccountInfo retrieves balances for all enabled currencies for the +// OKCoin exchange +func (o *OKCoin) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.ExchangeName = e.GetName() - assets, err := e.GetUserInfo() + response.ExchangeName = o.GetName() + assets, err := o.GetUserInfo() if err != nil { return response, err } diff --git a/exchanges/orderbook/orderbook.go b/exchanges/orderbook/orderbook.go index d9391a58..0236bc87 100644 --- a/exchanges/orderbook/orderbook.go +++ b/exchanges/orderbook/orderbook.go @@ -7,33 +7,44 @@ import ( "github.com/thrasher-/gocryptotrader/currency/pair" ) -var ( +// Const values for orderbook package +const ( ErrOrderbookForExchangeNotFound = "Ticker for exchange does not exist." ErrPrimaryCurrencyNotFound = "Error primary currency for orderbook not found." ErrSecondaryCurrencyNotFound = "Error secondary currency for orderbook not found." + Spot = "SPOT" +) + +// Vars for the orderbook package +var ( Orderbooks []Orderbook ) -type OrderbookItem struct { +// Item stores the amount and price values +type Item struct { Amount float64 Price float64 } -type OrderbookBase struct { +// Base holds the fields for the orderbook base +type Base struct { Pair pair.CurrencyPair `json:"pair"` CurrencyPair string `json:"CurrencyPair"` - Bids []OrderbookItem `json:"bids"` - Asks []OrderbookItem `json:"asks"` + Bids []Item `json:"bids"` + Asks []Item `json:"asks"` LastUpdated time.Time `json:"last_updated"` } +// Orderbook holds the orderbook information for a currency pair and type type Orderbook struct { - Orderbook map[pair.CurrencyItem]map[pair.CurrencyItem]OrderbookBase + Orderbook map[pair.CurrencyItem]map[pair.CurrencyItem]map[string]Base ExchangeName string } -func (o *OrderbookBase) CalculateTotalBids() (float64, float64) { +// CalculateTotalBids returns the total amount of bids and the total orderbook +// bids value +func (o *Base) CalculateTotalBids() (float64, float64) { amountCollated := float64(0) total := float64(0) for _, x := range o.Bids { @@ -43,7 +54,9 @@ func (o *OrderbookBase) CalculateTotalBids() (float64, float64) { return amountCollated, total } -func (o *OrderbookBase) CalculateTotalAsks() (float64, float64) { +// CalculateTotalAsks returns the total amount of asks and the total orderbook +// asks value +func (o *Base) CalculateTotalAsks() (float64, float64) { amountCollated := float64(0) total := float64(0) for _, x := range o.Asks { @@ -53,29 +66,33 @@ func (o *OrderbookBase) CalculateTotalAsks() (float64, float64) { return amountCollated, total } -func (o *OrderbookBase) Update(Bids, Asks []OrderbookItem) { +// Update updates the bids and asks +func (o *Base) Update(Bids, Asks []Item) { o.Bids = Bids o.Asks = Asks o.LastUpdated = time.Now() } -func GetOrderbook(exchange string, p pair.CurrencyPair) (OrderbookBase, error) { +// GetOrderbook checks and returns the orderbook given an exchange name and +// currency pair if it exists +func GetOrderbook(exchange string, p pair.CurrencyPair, orderbookType string) (Base, error) { orderbook, err := GetOrderbookByExchange(exchange) if err != nil { - return OrderbookBase{}, err + return Base{}, err } if !FirstCurrencyExists(exchange, p.GetFirstCurrency()) { - return OrderbookBase{}, errors.New(ErrPrimaryCurrencyNotFound) + return Base{}, errors.New(ErrPrimaryCurrencyNotFound) } if !SecondCurrencyExists(exchange, p) { - return OrderbookBase{}, errors.New(ErrSecondaryCurrencyNotFound) + return Base{}, errors.New(ErrSecondaryCurrencyNotFound) } - return orderbook.Orderbook[p.GetFirstCurrency()][p.GetSecondCurrency()], nil + return orderbook.Orderbook[p.GetFirstCurrency()][p.GetSecondCurrency()][orderbookType], nil } +// GetOrderbookByExchange returns an exchange orderbook func GetOrderbookByExchange(exchange string) (*Orderbook, error) { for _, y := range Orderbooks { if y.ExchangeName == exchange { @@ -85,6 +102,8 @@ func GetOrderbookByExchange(exchange string) (*Orderbook, error) { return nil, errors.New(ErrOrderbookForExchangeNotFound) } +// FirstCurrencyExists checks to see if the first currency of the orderbook map +// exists func FirstCurrencyExists(exchange string, currency pair.CurrencyItem) bool { for _, y := range Orderbooks { if y.ExchangeName == exchange { @@ -96,6 +115,8 @@ func FirstCurrencyExists(exchange string, currency pair.CurrencyItem) bool { return false } +// SecondCurrencyExists checks to see if the second currency of the orderbook +// map exists func SecondCurrencyExists(exchange string, p pair.CurrencyPair) bool { for _, y := range Orderbooks { if y.ExchangeName == exchange { @@ -109,39 +130,51 @@ func SecondCurrencyExists(exchange string, p pair.CurrencyPair) bool { return false } -func CreateNewOrderbook(exchangeName string, p pair.CurrencyPair, orderbookNew OrderbookBase) Orderbook { +// CreateNewOrderbook creates a new orderbook +func CreateNewOrderbook(exchangeName string, p pair.CurrencyPair, orderbookNew Base, orderbookType string) Orderbook { orderbook := Orderbook{} orderbook.ExchangeName = exchangeName - orderbook.Orderbook = make(map[pair.CurrencyItem]map[pair.CurrencyItem]OrderbookBase) - sMap := make(map[pair.CurrencyItem]OrderbookBase) - sMap[p.GetSecondCurrency()] = orderbookNew - orderbook.Orderbook[p.GetFirstCurrency()] = sMap + orderbook.Orderbook = make(map[pair.CurrencyItem]map[pair.CurrencyItem]map[string]Base) + a := make(map[pair.CurrencyItem]map[string]Base) + b := make(map[string]Base) + b[orderbookType] = orderbookNew + a[p.SecondCurrency] = b + orderbook.Orderbook[p.FirstCurrency] = a Orderbooks = append(Orderbooks, orderbook) return orderbook } -func ProcessOrderbook(exchangeName string, p pair.CurrencyPair, orderbookNew OrderbookBase) { +// ProcessOrderbook processes incoming orderbooks, creating or updating the +// Orderbook list +func ProcessOrderbook(exchangeName string, p pair.CurrencyPair, orderbookNew Base, orderbookType string) { orderbookNew.CurrencyPair = p.Pair().String() + orderbookNew.LastUpdated = time.Now() + if len(Orderbooks) == 0 { - CreateNewOrderbook(exchangeName, p, orderbookNew) + CreateNewOrderbook(exchangeName, p, orderbookNew, orderbookType) return - } else { - orderbook, err := GetOrderbookByExchange(exchangeName) - if err != nil { - CreateNewOrderbook(exchangeName, p, orderbookNew) + } + + orderbook, err := GetOrderbookByExchange(exchangeName) + if err != nil { + CreateNewOrderbook(exchangeName, p, orderbookNew, orderbookType) + return + } + + if FirstCurrencyExists(exchangeName, p.GetFirstCurrency()) { + if !SecondCurrencyExists(exchangeName, p) { + a := orderbook.Orderbook[p.FirstCurrency] + b := make(map[string]Base) + b[orderbookType] = orderbookNew + a[p.SecondCurrency] = b + orderbook.Orderbook[p.FirstCurrency] = a return } - - if FirstCurrencyExists(exchangeName, p.GetFirstCurrency()) { - if !SecondCurrencyExists(exchangeName, p) { - second := orderbook.Orderbook[p.GetFirstCurrency()] - second[p.GetSecondCurrency()] = orderbookNew - orderbook.Orderbook[p.GetFirstCurrency()] = second - return - } - } - sMap := make(map[pair.CurrencyItem]OrderbookBase) - sMap[p.GetSecondCurrency()] = orderbookNew - orderbook.Orderbook[p.GetFirstCurrency()] = sMap } + + a := make(map[pair.CurrencyItem]map[string]Base) + b := make(map[string]Base) + b[orderbookType] = orderbookNew + a[p.SecondCurrency] = b + orderbook.Orderbook[p.FirstCurrency] = a } diff --git a/exchanges/orderbook/orderbook_test.go b/exchanges/orderbook/orderbook_test.go new file mode 100644 index 00000000..d03e4c26 --- /dev/null +++ b/exchanges/orderbook/orderbook_test.go @@ -0,0 +1,266 @@ +package orderbook + +import ( + "testing" + "time" + + "github.com/thrasher-/gocryptotrader/currency/pair" +) + +func TestCalculateTotalBids(t *testing.T) { + t.Parallel() + currency := pair.NewCurrencyPair("BTC", "USD") + base := Base{ + Pair: currency, + CurrencyPair: currency.Pair().String(), + Bids: []Item{Item{Price: 100, Amount: 10}}, + LastUpdated: time.Now(), + } + + a, b := base.CalculateTotalBids() + if a != 10 && b != 1000 { + t.Fatal("Test failed. TestCalculateTotalBids expected a = 10 and b = 1000") + } +} + +func TestCalculateTotaAsks(t *testing.T) { + t.Parallel() + currency := pair.NewCurrencyPair("BTC", "USD") + base := Base{ + Pair: currency, + CurrencyPair: currency.Pair().String(), + Asks: []Item{Item{Price: 100, Amount: 10}}, + LastUpdated: time.Now(), + } + + a, b := base.CalculateTotalAsks() + if a != 10 && b != 1000 { + t.Fatal("Test failed. TestCalculateTotalAsks expected a = 10 and b = 1000") + } +} + +func TestUpdate(t *testing.T) { + t.Parallel() + currency := pair.NewCurrencyPair("BTC", "USD") + timeNow := time.Now() + base := Base{ + Pair: currency, + CurrencyPair: currency.Pair().String(), + Asks: []Item{Item{Price: 100, Amount: 10}}, + Bids: []Item{Item{Price: 200, Amount: 10}}, + LastUpdated: timeNow, + } + + asks := []Item{Item{Price: 200, Amount: 101}} + bids := []Item{Item{Price: 201, Amount: 100}} + time.Sleep(time.Millisecond * 50) + base.Update(bids, asks) + + if !base.LastUpdated.After(timeNow) { + t.Fatal("test failed. TestUpdate expected LastUpdated to be greater then original time") + } + + a, b := base.CalculateTotalAsks() + if a != 100 && b != 20200 { + t.Fatal("Test failed. TestUpdate expected a = 100 and b = 20100") + } + + a, b = base.CalculateTotalBids() + if a != 100 && b != 20100 { + t.Fatal("Test failed. TestUpdate expected a = 100 and b = 20100") + } +} + +func TestGetOrderbook(t *testing.T) { + currency := pair.NewCurrencyPair("BTC", "USD") + base := Base{ + Pair: currency, + CurrencyPair: currency.Pair().String(), + Asks: []Item{Item{Price: 100, Amount: 10}}, + Bids: []Item{Item{Price: 200, Amount: 10}}, + } + + CreateNewOrderbook("Exchange", currency, base, Spot) + + result, err := GetOrderbook("Exchange", currency, Spot) + if err != nil { + t.Fatalf("Test failed. TestGetOrderbook failed to get orderbook. Error %s", + err) + } + + if result.Pair.Pair() != currency.Pair() { + t.Fatal("Test failed. TestGetOrderbook failed. Mismatched pairs") + } + + _, err = GetOrderbook("nonexistant", currency, Spot) + if err == nil { + t.Fatal("Test failed. TestGetOrderbook retrieved non-existant orderbook") + } + + currency.FirstCurrency = "blah" + _, err = GetOrderbook("Exchange", currency, Spot) + if err == nil { + t.Fatal("Test failed. TestGetOrderbook retrieved non-existant orderbook using invalid first currency") + } + + newCurrency := pair.NewCurrencyPair("BTC", "AUD") + _, err = GetOrderbook("Exchange", newCurrency, Spot) + if err == nil { + t.Fatal("Test failed. TestGetOrderbook retrieved non-existant orderbook using invalid second currency") + } +} + +func TestGetOrderbookByExchange(t *testing.T) { + currency := pair.NewCurrencyPair("BTC", "USD") + base := Base{ + Pair: currency, + CurrencyPair: currency.Pair().String(), + Asks: []Item{Item{Price: 100, Amount: 10}}, + Bids: []Item{Item{Price: 200, Amount: 10}}, + } + + CreateNewOrderbook("Exchange", currency, base, Spot) + + _, err := GetOrderbookByExchange("Exchange") + if err != nil { + t.Fatalf("Test failed. TestGetOrderbookByExchange failed to get orderbook. Error %s", + err) + } + + _, err = GetOrderbookByExchange("nonexistant") + if err == nil { + t.Fatal("Test failed. TestGetOrderbookByExchange retrieved non-existant orderbook") + } +} + +func TestFirstCurrencyExists(t *testing.T) { + currency := pair.NewCurrencyPair("BTC", "AUD") + base := Base{ + Pair: currency, + CurrencyPair: currency.Pair().String(), + Asks: []Item{Item{Price: 100, Amount: 10}}, + Bids: []Item{Item{Price: 200, Amount: 10}}, + } + + CreateNewOrderbook("Exchange", currency, base, Spot) + + if !FirstCurrencyExists("Exchange", currency.FirstCurrency) { + t.Fatal("Test failed. TestFirstCurrencyExists expected first currency doesn't exist") + } + + var item pair.CurrencyItem = "blah" + if FirstCurrencyExists("Exchange", item) { + t.Fatal("Test failed. TestFirstCurrencyExists unexpected first currency exists") + } +} + +func TestSecondCurrencyExists(t *testing.T) { + currency := pair.NewCurrencyPair("BTC", "USD") + base := Base{ + Pair: currency, + CurrencyPair: currency.Pair().String(), + Asks: []Item{Item{Price: 100, Amount: 10}}, + Bids: []Item{Item{Price: 200, Amount: 10}}, + } + + CreateNewOrderbook("Exchange", currency, base, Spot) + + if !SecondCurrencyExists("Exchange", currency) { + t.Fatal("Test failed. TestSecondCurrencyExists expected first currency doesn't exist") + } + + currency.SecondCurrency = "blah" + if SecondCurrencyExists("Exchange", currency) { + t.Fatal("Test failed. TestSecondCurrencyExists unexpected first currency exists") + } +} + +func TestCreateNewOrderbook(t *testing.T) { + currency := pair.NewCurrencyPair("BTC", "USD") + base := Base{ + Pair: currency, + CurrencyPair: currency.Pair().String(), + Asks: []Item{Item{Price: 100, Amount: 10}}, + Bids: []Item{Item{Price: 200, Amount: 10}}, + } + + CreateNewOrderbook("Exchange", currency, base, Spot) + + result, err := GetOrderbook("Exchange", currency, Spot) + if err != nil { + t.Fatal("Test failed. TestCreateNewOrderbook failed to create new orderbook") + } + + if result.Pair.Pair() != currency.Pair() { + t.Fatal("Test failed. TestCreateNewOrderbook result pair is incorrect") + } + + a, b := result.CalculateTotalAsks() + if a != 10 && b != 1000 { + t.Fatal("Test failed. TestCreateNewOrderbook CalculateTotalAsks value is incorrect") + } + + a, b = result.CalculateTotalBids() + if a != 10 && b != 2000 { + t.Fatal("Test failed. TestCreateNewOrderbook CalculateTotalBids value is incorrect") + } +} + +func TestProcessOrderbook(t *testing.T) { + Orderbooks = []Orderbook{} + currency := pair.NewCurrencyPair("BTC", "USD") + base := Base{ + Pair: currency, + CurrencyPair: currency.Pair().String(), + Asks: []Item{Item{Price: 100, Amount: 10}}, + Bids: []Item{Item{Price: 200, Amount: 10}}, + } + + ProcessOrderbook("Exchange", currency, base, Spot) + + result, err := GetOrderbook("Exchange", currency, Spot) + if err != nil { + t.Fatal("Test failed. TestProcessOrderbook failed to create new orderbook") + } + + if result.Pair.Pair() != currency.Pair() { + t.Fatal("Test failed. TestProcessOrderbook result pair is incorrect") + } + + currency = pair.NewCurrencyPair("BTC", "GBP") + base.Pair = currency + ProcessOrderbook("Exchange", currency, base, Spot) + + result, err = GetOrderbook("Exchange", currency, Spot) + if err != nil { + t.Fatal("Test failed. TestProcessOrderbook failed to retrieve new orderbook") + } + + if result.Pair.Pair() != currency.Pair() { + t.Fatal("Test failed. TestProcessOrderbook result pair is incorrect") + } + + base.Asks = []Item{Item{Price: 200, Amount: 200}} + ProcessOrderbook("Exchange", currency, base, "monthly") + + result, err = GetOrderbook("Exchange", currency, "monthly") + if err != nil { + t.Fatal("Test failed. TestProcessOrderbook failed to retrieve new orderbook") + } + + a, b := result.CalculateTotalAsks() + if a != 200 && b != 40000 { + t.Fatal("Test failed. TestProcessOrderbook CalculateTotalsAsks incorrect values") + } + + base.Bids = []Item{Item{Price: 420, Amount: 200}} + ProcessOrderbook("Blah", currency, base, "quarterly") + result, err = GetOrderbook("Blah", currency, "quarterly") + if err != nil { + t.Fatal("Test failed. TestProcessOrderbook failed to create new orderbook") + } + + if a != 200 && b != 84000 { + t.Fatal("Test failed. TestProcessOrderbook CalculateTotalsBids incorrect values") + } +} diff --git a/exchanges/poloniex/poloniex.go b/exchanges/poloniex/poloniex.go index 0ce8e3b0..d0b8d753 100644 --- a/exchanges/poloniex/poloniex.go +++ b/exchanges/poloniex/poloniex.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "log" "net/url" "strconv" "time" @@ -11,6 +12,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) const ( @@ -57,6 +59,11 @@ func (p *Poloniex) SetDefaults() { p.Verbose = false p.Websocket = false p.RESTPollingDelay = 10 + p.RequestCurrencyPairFormat.Delimiter = "_" + p.RequestCurrencyPairFormat.Uppercase = true + p.ConfigCurrencyPairFormat.Delimiter = "_" + p.ConfigCurrencyPairFormat.Uppercase = true + p.AssetTypes = []string{ticker.Spot} } func (p *Poloniex) Setup(exch config.ExchangeConfig) { @@ -72,6 +79,14 @@ func (p *Poloniex) Setup(exch config.ExchangeConfig) { p.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") p.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") p.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := p.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = p.SetAssetTypes() + if err != nil { + log.Fatal(err) + } } } @@ -86,7 +101,7 @@ func (p *Poloniex) GetTicker() (map[string]PoloniexTicker, error) { resp := response{} path := fmt.Sprintf("%s/public?command=returnTicker", POLONIEX_API_URL) - err := common.SendHTTPGetRequest(path, true, &resp.Data) + err := common.SendHTTPGetRequest(path, true, p.Verbose, &resp.Data) if err != nil { return resp.Data, err @@ -97,7 +112,7 @@ func (p *Poloniex) GetTicker() (map[string]PoloniexTicker, error) { func (p *Poloniex) GetVolume() (interface{}, error) { var resp interface{} path := fmt.Sprintf("%s/public?command=return24hVolume", POLONIEX_API_URL) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, p.Verbose, &resp) if err != nil { return resp, err @@ -115,7 +130,7 @@ func (p *Poloniex) GetOrderbook(currencyPair string, depth int) (PoloniexOrderbo resp := PoloniexOrderbookResponse{} path := fmt.Sprintf("%s/public?command=returnOrderBook&%s", POLONIEX_API_URL, vals.Encode()) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, p.Verbose, &resp) if err != nil { return PoloniexOrderbook{}, err @@ -124,14 +139,20 @@ func (p *Poloniex) GetOrderbook(currencyPair string, depth int) (PoloniexOrderbo ob := PoloniexOrderbook{} for x := range resp.Asks { data := resp.Asks[x] - price, _ := strconv.ParseFloat(data[0].(string), 64) + price, err := strconv.ParseFloat(data[0].(string), 64) + if err != nil { + return ob, err + } amount := data[1].(float64) ob.Asks = append(ob.Asks, PoloniexOrderbookItem{Price: price, Amount: amount}) } for x := range resp.Bids { data := resp.Bids[x] - price, _ := strconv.ParseFloat(data[0].(string), 64) + price, err := strconv.ParseFloat(data[0].(string), 64) + if err != nil { + return ob, err + } amount := data[1].(float64) ob.Bids = append(ob.Bids, PoloniexOrderbookItem{Price: price, Amount: amount}) } @@ -152,7 +173,7 @@ func (p *Poloniex) GetTradeHistory(currencyPair, start, end string) ([]PoloniexT resp := []PoloniexTradeHistory{} path := fmt.Sprintf("%s/public?command=returnTradeHistory&%s", POLONIEX_API_URL, vals.Encode()) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, p.Verbose, &resp) if err != nil { return nil, err @@ -178,7 +199,7 @@ func (p *Poloniex) GetChartData(currencyPair, start, end, period string) ([]Polo resp := []PoloniexChartData{} path := fmt.Sprintf("%s/public?command=returnChartData&%s", POLONIEX_API_URL, vals.Encode()) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, p.Verbose, &resp) if err != nil { return nil, err @@ -192,7 +213,7 @@ func (p *Poloniex) GetCurrencies() (map[string]PoloniexCurrencies, error) { } resp := Response{} path := fmt.Sprintf("%s/public?command=returnCurrencies", POLONIEX_API_URL) - err := common.SendHTTPGetRequest(path, true, &resp.Data) + err := common.SendHTTPGetRequest(path, true, p.Verbose, &resp.Data) if err != nil { return resp.Data, err @@ -203,7 +224,7 @@ func (p *Poloniex) GetCurrencies() (map[string]PoloniexCurrencies, error) { func (p *Poloniex) GetLoanOrders(currency string) (PoloniexLoanOrders, error) { resp := PoloniexLoanOrders{} path := fmt.Sprintf("%s/public?command=returnLoanOrders¤cy=%s", POLONIEX_API_URL, currency) - err := common.SendHTTPGetRequest(path, true, &resp) + err := common.SendHTTPGetRequest(path, true, p.Verbose, &resp) if err != nil { return resp, err diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 1c4220f3..78d7a480 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -2,20 +2,20 @@ package poloniex import ( "log" - "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) +// Start starts the Poloniex go routine func (p *Poloniex) Start() { go p.Run() } +// Run implements the Poloniex wrapper func (p *Poloniex) Run() { if p.Verbose { log.Printf("%s Websocket: %s (url: %s).\n", p.GetName(), common.IsEnabled(p.Websocket), POLONIEX_WEBSOCKET_ADDRESS) @@ -26,80 +26,77 @@ func (p *Poloniex) Run() { if p.Websocket { go p.WebsocketClient() } - - for p.Enabled { - for _, x := range p.EnabledPairs { - currency := pair.NewCurrencyPairDelimiter(x, "_") - go func() { - ticker, err := p.GetTickerPrice(currency) - if err != nil { - log.Println(err) - return - } - log.Printf("Poloniex %s Last %f High %f Low %f Volume %f\n", exchange.FormatCurrency(currency).String(), ticker.Last, ticker.High, ticker.Low, ticker.Volume) - stats.AddExchangeInfo(p.GetName(), currency.GetFirstCurrency().String(), currency.GetSecondCurrency().String(), ticker.Last, ticker.Volume) - }() - } - time.Sleep(time.Second * p.RESTPollingDelay) - } } -func (p *Poloniex) GetTickerPrice(currencyPair pair.CurrencyPair) (ticker.TickerPrice, error) { - currency := currencyPair.Pair().String() - tickerNew, err := ticker.GetTicker(p.GetName(), currencyPair) - if err == nil { - return tickerNew, nil - } - - var tickerPrice ticker.TickerPrice +// UpdateTicker updates and returns the ticker for a currency pair +func (p *Poloniex) UpdateTicker(currencyPair pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price tick, err := p.GetTicker() if err != nil { return tickerPrice, err } - tickerPrice.Pair = currencyPair - tickerPrice.Ask = tick[currency].Last - tickerPrice.Bid = tick[currency].HighestBid - tickerPrice.High = tick[currency].HighestBid - tickerPrice.Last = tick[currency].Last - tickerPrice.Low = tick[currency].LowestAsk - tickerPrice.Volume = tick[currency].BaseVolume - ticker.ProcessTicker(p.GetName(), currencyPair, tickerPrice) - return tickerPrice, nil + for _, x := range p.GetEnabledCurrencies() { + var tp ticker.Price + curr := exchange.FormatExchangeCurrency(p.GetName(), x).String() + tp.Pair = x + tp.Ask = tick[curr].LowestAsk + tp.Bid = tick[curr].HighestBid + tp.High = tick[curr].High24Hr + tp.Last = tick[curr].Last + tp.Low = tick[curr].Low24Hr + tp.Volume = tick[curr].BaseVolume + ticker.ProcessTicker(p.GetName(), x, tp, assetType) + } + return ticker.GetTicker(p.Name, currencyPair, assetType) } -func (p *Poloniex) GetOrderbookEx(currencyPair pair.CurrencyPair) (orderbook.OrderbookBase, error) { - currency := currencyPair.Pair().String() - ob, err := orderbook.GetOrderbook(p.GetName(), currencyPair) - if err == nil { - return ob, nil +// GetTickerPrice returns the ticker for a currency pair +func (p *Poloniex) GetTickerPrice(currencyPair pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(p.GetName(), currencyPair, assetType) + if err != nil { + return p.UpdateTicker(currencyPair, assetType) } + return tickerNew, nil +} - var orderBook orderbook.OrderbookBase - orderbookNew, err := p.GetOrderbook(currency, 1000) +// GetOrderbookEx returns orderbook base on the currency pair +func (p *Poloniex) GetOrderbookEx(currencyPair pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(p.GetName(), currencyPair, assetType) + if err == nil { + return p.UpdateOrderbook(currencyPair, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (p *Poloniex) UpdateOrderbook(currencyPair pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + orderbookNew, err := p.GetOrderbook(exchange.FormatExchangeCurrency(p.GetName(), currencyPair).String(), 1000) if err != nil { return orderBook, err } for x := range orderbookNew.Bids { data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.OrderbookItem{Amount: data.Amount, Price: data.Price}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data.Amount, Price: data.Price}) } for x := range orderbookNew.Asks { data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.OrderbookItem{Amount: data.Amount, Price: data.Price}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data.Amount, Price: data.Price}) } - orderBook.Pair = currencyPair - orderbook.ProcessOrderbook(p.GetName(), currencyPair, orderBook) - return orderBook, nil + + orderbook.ProcessOrderbook(p.GetName(), currencyPair, orderBook, assetType) + return orderbook.GetOrderbook(p.Name, currencyPair, assetType) } -//GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the Poloniex exchange -func (e *Poloniex) GetExchangeAccountInfo() (exchange.AccountInfo, error) { +// GetExchangeAccountInfo retrieves balances for all enabled currencies for the +// Poloniex exchange +func (p *Poloniex) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.ExchangeName = e.GetName() - accountBalance, err := e.GetBalances() + response.ExchangeName = p.GetName() + accountBalance, err := p.GetBalances() if err != nil { return response, err } diff --git a/exchanges/stats/stats.go b/exchanges/stats/stats.go index 5d477e62..200a35fa 100644 --- a/exchanges/stats/stats.go +++ b/exchanges/stats/stats.go @@ -3,113 +3,134 @@ package stats import ( "sort" - "github.com/thrasher-/gocryptotrader/currency" + "github.com/thrasher-/gocryptotrader/currency/pair" ) -type ExchangeInfo struct { - Exchange string - FirstCurrency string - FiatCurrency string - Price float64 - Volume float64 +// Item holds various fields for storing currency pair stats +type Item struct { + Exchange string + Pair pair.CurrencyPair + AssetType string + Price float64 + Volume float64 } -var ExchInfo []ExchangeInfo +// Items var array +var Items []Item -type ByPrice []ExchangeInfo +// ByPrice allows sorting by price +type ByPrice []Item -func (this ByPrice) Len() int { - return len(this) +func (b ByPrice) Len() int { + return len(b) } -func (this ByPrice) Less(i, j int) bool { - return this[i].Price < this[j].Price +func (b ByPrice) Less(i, j int) bool { + return b[i].Price < b[j].Price } -func (this ByPrice) Swap(i, j int) { - this[i], this[j] = this[j], this[i] +func (b ByPrice) Swap(i, j int) { + b[i], b[j] = b[j], b[i] } -type ByVolume []ExchangeInfo +// ByVolume allows sorting by volume +type ByVolume []Item -func (this ByVolume) Len() int { - return len(this) +func (b ByVolume) Len() int { + return len(b) } -func (this ByVolume) Less(i, j int) bool { - return this[i].Volume < this[j].Volume +func (b ByVolume) Less(i, j int) bool { + return b[i].Volume < b[j].Volume } -func (this ByVolume) Swap(i, j int) { - this[i], this[j] = this[j], this[i] +func (b ByVolume) Swap(i, j int) { + b[i], b[j] = b[j], b[i] } -func AddExchangeInfo(exchange, crypto, fiat string, price, volume float64) { - if currency.BaseCurrencies == "" { - currency.BaseCurrencies = currency.DefaultCurrencies - } - - if !currency.IsFiatCurrency(fiat) { - return - } - AppendExchangeInfo(exchange, crypto, fiat, price, volume) - -} - -func AppendExchangeInfo(exchange, crypto, fiat string, price, volume float64) { - if ExchangeInfoAlreadyExists(exchange, crypto, fiat, price, volume) { +// Add adds or updates the item stats +func Add(exchange string, p pair.CurrencyPair, assetType string, price, volume float64) { + if exchange == "" || assetType == "" || price == 0 || volume == 0 || p.FirstCurrency == "" || p.SecondCurrency == "" { return } - exch := ExchangeInfo{} - exch.Exchange = exchange - exch.FirstCurrency = crypto - exch.FiatCurrency = fiat - exch.Price = price - exch.Volume = volume - ExchInfo = append(ExchInfo, exch) + if p.FirstCurrency == "XBT" { + newPair := pair.NewCurrencyPair("BTC", p.SecondCurrency.String()) + Append(exchange, newPair, assetType, price, volume) + } + + if p.SecondCurrency == "USDT" { + newPair := pair.NewCurrencyPair(p.FirstCurrency.String(), "USD") + Append(exchange, newPair, assetType, price, volume) + } + + Append(exchange, p, assetType, price, volume) } -func ExchangeInfoAlreadyExists(exchange, crypto, fiat string, price, volume float64) bool { - for i := range ExchInfo { - if ExchInfo[i].Exchange == exchange && ExchInfo[i].FirstCurrency == crypto && ExchInfo[i].FiatCurrency == fiat { - ExchInfo[i].Price, ExchInfo[i].Volume = price, volume +// Append adds or updates the item stats for a specific +// currency pair and asset type +func Append(exchange string, p pair.CurrencyPair, assetType string, price, volume float64) { + if AlreadyExists(exchange, p, assetType, price, volume) { + return + } + + i := Item{ + Exchange: exchange, + Pair: p, + AssetType: assetType, + Price: price, + Volume: volume, + } + + Items = append(Items, i) +} + +// AlreadyExists checks to see if item info already exists +// for a specific currency pair and asset type +func AlreadyExists(exchange string, p pair.CurrencyPair, assetType string, price, volume float64) bool { + for i := range Items { + if Items[i].Exchange == exchange && Items[i].Pair.Equal(p) && Items[i].AssetType == assetType { + Items[i].Price, Items[i].Volume = price, volume return true } } return false } -func SortExchangesByVolume(crypto, fiat string, reverse bool) []ExchangeInfo { - info := []ExchangeInfo{} - - for _, x := range ExchInfo { - if x.FirstCurrency == crypto && x.FiatCurrency == fiat { - info = append(info, x) +// SortExchangesByVolume sorts item info by volume for a specific +// currency pair and asset type. Reverse will reverse the order from lowest to +// highest +func SortExchangesByVolume(p pair.CurrencyPair, assetType string, reverse bool) []Item { + var result []Item + for x := range Items { + if Items[x].Pair.Equal(p) && Items[x].AssetType == assetType { + result = append(result, Items[x]) } } if reverse { - sort.Sort(sort.Reverse(ByVolume(info))) + sort.Sort(sort.Reverse(ByVolume(result))) } else { - sort.Sort(ByVolume(info)) + sort.Sort(ByVolume(result)) } - return info + return result } -func SortExchangesByPrice(crypto, fiat string, reverse bool) []ExchangeInfo { - info := []ExchangeInfo{} - - for _, x := range ExchInfo { - if x.FirstCurrency == crypto && x.FiatCurrency == fiat { - info = append(info, x) +// SortExchangesByPrice sorts item info by volume for a specific +// currency pair and asset type. Reverse will reverse the order from lowest to +// highest +func SortExchangesByPrice(p pair.CurrencyPair, assetType string, reverse bool) []Item { + var result []Item + for x := range Items { + if Items[x].Pair.Equal(p) && Items[x].AssetType == assetType { + result = append(result, Items[x]) } } if reverse { - sort.Sort(sort.Reverse(ByPrice(info))) + sort.Sort(sort.Reverse(ByPrice(result))) } else { - sort.Sort(ByPrice(info)) + sort.Sort(ByPrice(result)) } - return info + return result } diff --git a/exchanges/stats/stats_test.go b/exchanges/stats/stats_test.go index 812823ef..db3c17fa 100644 --- a/exchanges/stats/stats_test.go +++ b/exchanges/stats/stats_test.go @@ -2,138 +2,180 @@ package stats import ( "testing" + + "github.com/thrasher-/gocryptotrader/currency/pair" ) func TestLenByPrice(t *testing.T) { - exchangeInfo := ExchangeInfo{ - Exchange: "ANX", - FirstCurrency: "BTC", - FiatCurrency: "USD", - Price: 1200, - Volume: 5, + p := pair.NewCurrencyPair("BTC", "USD") + i := Item{ + Exchange: "ANX", + Pair: p, + AssetType: "SPOT", + Price: 1200, + Volume: 5, } - ExchInfo = append(ExchInfo, exchangeInfo) - if ByPrice.Len(ExchInfo) < 1 { + Items = append(Items, i) + if ByPrice.Len(Items) < 1 { t.Error("Test Failed - stats LenByPrice() length not correct.") } } func TestLessByPrice(t *testing.T) { - exchangeInfo := ExchangeInfo{ - Exchange: "alphapoint", - FirstCurrency: "BTC", - FiatCurrency: "USD", - Price: 1200, - Volume: 5, + p := pair.NewCurrencyPair("BTC", "USD") + i := Item{ + Exchange: "alphapoint", + Pair: p, + AssetType: "SPOT", + Price: 1200, + Volume: 5, } - exchangeInfo2 := ExchangeInfo{ - Exchange: "bitfinex", - FirstCurrency: "BTC", - FiatCurrency: "USD", - Price: 1198, - Volume: 20, + i2 := Item{ + Exchange: "bitfinex", + Pair: p, + AssetType: "SPOT", + Price: 1198, + Volume: 20, } - ExchInfo = append(ExchInfo, exchangeInfo) - ExchInfo = append(ExchInfo, exchangeInfo2) + Items = append(Items, i) + Items = append(Items, i2) - if !ByPrice.Less(ExchInfo, 2, 1) { + if !ByPrice.Less(Items, 2, 1) { t.Error("Test Failed - stats LessByPrice() incorrect return.") } - if ByPrice.Less(ExchInfo, 1, 2) { + if ByPrice.Less(Items, 1, 2) { t.Error("Test Failed - stats LessByPrice() incorrect return.") } } func TestSwapByPrice(t *testing.T) { - exchangeInfo := ExchangeInfo{ - Exchange: "bitstamp", - FirstCurrency: "BTC", - FiatCurrency: "USD", - Price: 1324, - Volume: 5, + p := pair.NewCurrencyPair("BTC", "USD") + i := Item{ + Exchange: "bitstamp", + Pair: p, + AssetType: "SPOT", + Price: 1324, + Volume: 5, } - exchangeInfo2 := ExchangeInfo{ - Exchange: "btcc", - FirstCurrency: "BTC", - FiatCurrency: "USD", - Price: 7863, - Volume: 20, + i2 := Item{ + Exchange: "btcc", + Pair: p, + AssetType: "SPOT", + Price: 7863, + Volume: 20, } - ExchInfo = append(ExchInfo, exchangeInfo) - ExchInfo = append(ExchInfo, exchangeInfo2) - ByPrice.Swap(ExchInfo, 3, 4) - if ExchInfo[3].Exchange != "btcc" || ExchInfo[4].Exchange != "bitstamp" { + Items = append(Items, i) + Items = append(Items, i2) + ByPrice.Swap(Items, 3, 4) + if Items[3].Exchange != "btcc" || Items[4].Exchange != "bitstamp" { t.Error("Test Failed - stats SwapByPrice did not swap values.") } } func TestLenByVolume(t *testing.T) { - if ByVolume.Len(ExchInfo) != 5 { + if ByVolume.Len(Items) != 5 { t.Error("Test Failed - stats lenByVolume did not swap values.") } } func TestLessByVolume(t *testing.T) { - if !ByVolume.Less(ExchInfo, 1, 2) { + if !ByVolume.Less(Items, 1, 2) { t.Error("Test Failed - stats LessByVolume() incorrect return.") } - if ByVolume.Less(ExchInfo, 2, 1) { + if ByVolume.Less(Items, 2, 1) { t.Error("Test Failed - stats LessByVolume() incorrect return.") } } func TestSwapByVolume(t *testing.T) { - ByPrice.Swap(ExchInfo, 3, 4) + ByPrice.Swap(Items, 3, 4) - if ExchInfo[4].Exchange != "btcc" || ExchInfo[3].Exchange != "bitstamp" { + if Items[4].Exchange != "btcc" || Items[3].Exchange != "bitstamp" { t.Error("Test Failed - stats SwapByVolume did not swap values.") } } -func TestAddExchangeInfo(t *testing.T) { - ExchInfo = ExchInfo[:0] - AddExchangeInfo("ANX", "BTC", "USD", 1200, 42) +func TestAdd(t *testing.T) { + Items = Items[:0] + p := pair.NewCurrencyPair("BTC", "USD") + Add("ANX", p, "SPOT", 1200, 42) - if len(ExchInfo) < 1 { - t.Error("Test Failed - stats AddExchangeInfo did not add exchange info.") + if len(Items) < 1 { + t.Error("Test Failed - stats Add did not add exchange info.") + } + + Add("", p, "", 0, 0) + + if len(Items) != 1 { + t.Error("Test Failed - stats Add did not add exchange info.") + } + + p.FirstCurrency = "XBT" + Add("ANX", p, "SPOT", 1201, 43) + + if Items[1].Pair.Pair() != "XBTUSD" { + t.Fatal("Test failed. stats Add did not add exchange info.") + } + + p = pair.NewCurrencyPair("ETH", "USDT") + Add("ANX", p, "SPOT", 300, 1000) + + if Items[2].Pair.Pair() != "ETHUSD" { + t.Fatal("Test failed. stats Add did not add exchange info.") } } -func TestAppendExchangeInfo(t *testing.T) { - AppendExchangeInfo("sillyexchange", "BTC", "USD", 1234, 45) - if len(ExchInfo) < 2 { - t.Error("Test Failed - stats AppendExchangeInfo did not add exchange values.") +func TestAppend(t *testing.T) { + p := pair.NewCurrencyPair("BTC", "USD") + Append("sillyexchange", p, "SPOT", 1234, 45) + if len(Items) < 2 { + t.Error("Test Failed - stats Append did not add exchange values.") } - AppendExchangeInfo("sillyexchange", "BTC", "USD", 1234, 45) - if len(ExchInfo) == 3 { - t.Error("Test Failed - stats AppendExchangeInfo added exchange values") + + Append("sillyexchange", p, "SPOT", 1234, 45) + if len(Items) == 3 { + t.Error("Test Failed - stats Append added exchange values") } } -func TestExchangeInfoAlreadyExists(t *testing.T) { - if !ExchangeInfoAlreadyExists("ANX", "BTC", "USD", 1200, 42) { - t.Error("Test Failed - stats ExchangeInfoAlreadyExists exchange does not exist.") +func TestAlreadyExists(t *testing.T) { + p := pair.NewCurrencyPair("BTC", "USD") + if !AlreadyExists("ANX", p, "SPOT", 1200, 42) { + t.Error("Test Failed - stats AlreadyExists exchange does not exist.") } - if ExchangeInfoAlreadyExists("bla", "dii", "USD", 1234, 123) { - t.Error("Test Failed - stats ExchangeInfoAlreadyExists found incorrect exchange.") + p.FirstCurrency = "dii" + if AlreadyExists("bla", p, "SPOT", 1234, 123) { + t.Error("Test Failed - stats AlreadyExists found incorrect exchange.") } } func TestSortExchangesByVolume(t *testing.T) { - topVolume := SortExchangesByVolume("BTC", "USD", true) + p := pair.NewCurrencyPair("BTC", "USD") + topVolume := SortExchangesByVolume(p, "SPOT", true) if topVolume[0].Exchange != "sillyexchange" { t.Error("Test Failed - stats SortExchangesByVolume incorrectly sorted values.") } + + topVolume = SortExchangesByVolume(p, "SPOT", false) + if topVolume[0].Exchange != "ANX" { + t.Error("Test Failed - stats SortExchangesByVolume incorrectly sorted values.") + } } func TestSortExchangesByPrice(t *testing.T) { - topPrice := SortExchangesByPrice("BTC", "USD", true) + p := pair.NewCurrencyPair("BTC", "USD") + topPrice := SortExchangesByPrice(p, "SPOT", true) if topPrice[0].Exchange != "sillyexchange" { t.Error("Test Failed - stats SortExchangesByPrice incorrectly sorted values.") } + + topPrice = SortExchangesByPrice(p, "SPOT", false) + if topPrice[0].Exchange != "ANX" { + t.Error("Test Failed - stats SortExchangesByPrice incorrectly sorted values.") + } } diff --git a/exchanges/ticker/ticker.go b/exchanges/ticker/ticker.go index 576e13d7..3d2c23c7 100644 --- a/exchanges/ticker/ticker.go +++ b/exchanges/ticker/ticker.go @@ -8,15 +8,22 @@ import ( "github.com/thrasher-/gocryptotrader/currency/pair" ) -var ( +// Const values for the ticker package +const ( ErrTickerForExchangeNotFound = "Ticker for exchange does not exist." ErrPrimaryCurrencyNotFound = "Error primary currency for ticker not found." ErrSecondaryCurrencyNotFound = "Error secondary currency for ticker not found." + Spot = "SPOT" +) + +// Vars for the ticker package +var ( Tickers []Ticker ) -type TickerPrice struct { +// Price struct stores the currency pair and pricing information +type Price struct { Pair pair.CurrencyPair `json:"Pair"` CurrencyPair string `json:"CurrencyPair"` Last float64 `json:"Last"` @@ -28,50 +35,55 @@ type TickerPrice struct { PriceATH float64 `json:"PriceATH"` } +// Ticker struct holds the ticker information for a currency pair and type type Ticker struct { - Price map[pair.CurrencyItem]map[pair.CurrencyItem]TickerPrice + Price map[pair.CurrencyItem]map[pair.CurrencyItem]map[string]Price ExchangeName string } -func (t *Ticker) PriceToString(p pair.CurrencyPair, priceType string) string { +// PriceToString returns the string version of a stored price field +func (t *Ticker) PriceToString(p pair.CurrencyPair, priceType, tickerType string) string { priceType = common.StringToLower(priceType) + switch priceType { case "last": - return strconv.FormatFloat(t.Price[p.GetFirstCurrency()][p.GetSecondCurrency()].Last, 'f', -1, 64) + return strconv.FormatFloat(t.Price[p.FirstCurrency][p.SecondCurrency][tickerType].Last, 'f', -1, 64) case "high": - return strconv.FormatFloat(t.Price[p.GetFirstCurrency()][p.GetSecondCurrency()].High, 'f', -1, 64) + return strconv.FormatFloat(t.Price[p.FirstCurrency][p.SecondCurrency][tickerType].High, 'f', -1, 64) case "low": - return strconv.FormatFloat(t.Price[p.GetFirstCurrency()][p.GetSecondCurrency()].Low, 'f', -1, 64) + return strconv.FormatFloat(t.Price[p.FirstCurrency][p.SecondCurrency][tickerType].Low, 'f', -1, 64) case "bid": - return strconv.FormatFloat(t.Price[p.GetFirstCurrency()][p.GetSecondCurrency()].Bid, 'f', -1, 64) + return strconv.FormatFloat(t.Price[p.FirstCurrency][p.SecondCurrency][tickerType].Bid, 'f', -1, 64) case "ask": - return strconv.FormatFloat(t.Price[p.GetFirstCurrency()][p.GetSecondCurrency()].Ask, 'f', -1, 64) + return strconv.FormatFloat(t.Price[p.FirstCurrency][p.SecondCurrency][tickerType].Ask, 'f', -1, 64) case "volume": - return strconv.FormatFloat(t.Price[p.GetFirstCurrency()][p.GetSecondCurrency()].Volume, 'f', -1, 64) + return strconv.FormatFloat(t.Price[p.FirstCurrency][p.SecondCurrency][tickerType].Volume, 'f', -1, 64) case "ath": - return strconv.FormatFloat(t.Price[p.GetFirstCurrency()][p.GetSecondCurrency()].PriceATH, 'f', -1, 64) + return strconv.FormatFloat(t.Price[p.FirstCurrency][p.SecondCurrency][tickerType].PriceATH, 'f', -1, 64) default: return "" } } -func GetTicker(exchange string, p pair.CurrencyPair) (TickerPrice, error) { +// GetTicker checks and returns a requested ticker if it exists +func GetTicker(exchange string, p pair.CurrencyPair, tickerType string) (Price, error) { ticker, err := GetTickerByExchange(exchange) if err != nil { - return TickerPrice{}, err + return Price{}, err } - if !FirstCurrencyExists(exchange, p.GetFirstCurrency()) { - return TickerPrice{}, errors.New(ErrPrimaryCurrencyNotFound) + if !FirstCurrencyExists(exchange, p.FirstCurrency) { + return Price{}, errors.New(ErrPrimaryCurrencyNotFound) } if !SecondCurrencyExists(exchange, p) { - return TickerPrice{}, errors.New(ErrSecondaryCurrencyNotFound) + return Price{}, errors.New(ErrSecondaryCurrencyNotFound) } - return ticker.Price[p.GetFirstCurrency()][p.GetSecondCurrency()], nil + return ticker.Price[p.FirstCurrency][p.SecondCurrency][tickerType], nil } +// GetTickerByExchange returns an exchange Ticker func GetTickerByExchange(exchange string) (*Ticker, error) { for _, y := range Tickers { if y.ExchangeName == exchange { @@ -81,6 +93,8 @@ func GetTickerByExchange(exchange string) (*Ticker, error) { return nil, errors.New(ErrTickerForExchangeNotFound) } +// FirstCurrencyExists checks to see if the first currency of the Price map +// exists func FirstCurrencyExists(exchange string, currency pair.CurrencyItem) bool { for _, y := range Tickers { if y.ExchangeName == exchange { @@ -92,6 +106,8 @@ func FirstCurrencyExists(exchange string, currency pair.CurrencyItem) bool { return false } +// SecondCurrencyExists checks to see if the second currency of the Price map +// exists func SecondCurrencyExists(exchange string, p pair.CurrencyPair) bool { for _, y := range Tickers { if y.ExchangeName == exchange { @@ -105,40 +121,49 @@ func SecondCurrencyExists(exchange string, p pair.CurrencyPair) bool { return false } -func CreateNewTicker(exchangeName string, p pair.CurrencyPair, tickerNew TickerPrice) Ticker { +// CreateNewTicker creates a new Ticker +func CreateNewTicker(exchangeName string, p pair.CurrencyPair, tickerNew Price, tickerType string) Ticker { ticker := Ticker{} ticker.ExchangeName = exchangeName - ticker.Price = make(map[pair.CurrencyItem]map[pair.CurrencyItem]TickerPrice) - sMap := make(map[pair.CurrencyItem]TickerPrice) - sMap[p.GetSecondCurrency()] = tickerNew - ticker.Price[p.GetFirstCurrency()] = sMap + ticker.Price = make(map[pair.CurrencyItem]map[pair.CurrencyItem]map[string]Price) + a := make(map[pair.CurrencyItem]map[string]Price) + b := make(map[string]Price) + b[tickerType] = tickerNew + a[p.SecondCurrency] = b + ticker.Price[p.FirstCurrency] = a Tickers = append(Tickers, ticker) return ticker } -func ProcessTicker(exchangeName string, p pair.CurrencyPair, tickerNew TickerPrice) { +// ProcessTicker processes incoming tickers, creating or updating the Tickers +// list +func ProcessTicker(exchangeName string, p pair.CurrencyPair, tickerNew Price, tickerType string) { tickerNew.CurrencyPair = p.Pair().String() if len(Tickers) == 0 { - CreateNewTicker(exchangeName, p, tickerNew) - //issue - not appending + CreateNewTicker(exchangeName, p, tickerNew, tickerType) return - } else { - ticker, err := GetTickerByExchange(exchangeName) - if err != nil { - CreateNewTicker(exchangeName, p, tickerNew) + } + + ticker, err := GetTickerByExchange(exchangeName) + if err != nil { + CreateNewTicker(exchangeName, p, tickerNew, tickerType) + return + } + + if FirstCurrencyExists(exchangeName, p.FirstCurrency) { + if !SecondCurrencyExists(exchangeName, p) { + a := ticker.Price[p.FirstCurrency] + b := make(map[string]Price) + b[tickerType] = tickerNew + a[p.SecondCurrency] = b + ticker.Price[p.FirstCurrency] = a return } - - if FirstCurrencyExists(exchangeName, p.GetFirstCurrency()) { - if !SecondCurrencyExists(exchangeName, p) { - second := ticker.Price[p.GetFirstCurrency()] - second[p.GetSecondCurrency()] = tickerNew - ticker.Price[p.GetFirstCurrency()] = second - return - } - } - sMap := make(map[pair.CurrencyItem]TickerPrice) - sMap[p.GetSecondCurrency()] = tickerNew - ticker.Price[p.GetFirstCurrency()] = sMap } + + a := make(map[pair.CurrencyItem]map[string]Price) + b := make(map[string]Price) + b[tickerType] = tickerNew + a[p.SecondCurrency] = b + ticker.Price[p.FirstCurrency] = a } diff --git a/exchanges/ticker/ticker_test.go b/exchanges/ticker/ticker_test.go index 789a751b..7f5d29ed 100644 --- a/exchanges/ticker/ticker_test.go +++ b/exchanges/ticker/ticker_test.go @@ -9,7 +9,7 @@ import ( func TestPriceToString(t *testing.T) { newPair := pair.NewCurrencyPair("BTC", "USD") - priceStruct := TickerPrice{ + priceStruct := Price{ Pair: newPair, CurrencyPair: newPair.Pair().String(), Last: 1200, @@ -21,37 +21,37 @@ func TestPriceToString(t *testing.T) { PriceATH: 1337, } - newTicker := CreateNewTicker("ANX", newPair, priceStruct) + newTicker := CreateNewTicker("ANX", newPair, priceStruct, Spot) - if newTicker.PriceToString(newPair, "last") != "1200" { + if newTicker.PriceToString(newPair, "last", Spot) != "1200" { t.Error("Test Failed - ticker PriceToString last value is incorrect") } - if newTicker.PriceToString(newPair, "high") != "1298" { + if newTicker.PriceToString(newPair, "high", Spot) != "1298" { t.Error("Test Failed - ticker PriceToString high value is incorrect") } - if newTicker.PriceToString(newPair, "low") != "1148" { + if newTicker.PriceToString(newPair, "low", Spot) != "1148" { t.Error("Test Failed - ticker PriceToString low value is incorrect") } - if newTicker.PriceToString(newPair, "bid") != "1195" { + if newTicker.PriceToString(newPair, "bid", Spot) != "1195" { t.Error("Test Failed - ticker PriceToString bid value is incorrect") } - if newTicker.PriceToString(newPair, "ask") != "1220" { + if newTicker.PriceToString(newPair, "ask", Spot) != "1220" { t.Error("Test Failed - ticker PriceToString ask value is incorrect") } - if newTicker.PriceToString(newPair, "volume") != "5" { + if newTicker.PriceToString(newPair, "volume", Spot) != "5" { t.Error("Test Failed - ticker PriceToString volume value is incorrect") } - if newTicker.PriceToString(newPair, "ath") != "1337" { + if newTicker.PriceToString(newPair, "ath", Spot) != "1337" { t.Error("Test Failed - ticker PriceToString ath value is incorrect") } - if newTicker.PriceToString(newPair, "obtuse") != "" { + if newTicker.PriceToString(newPair, "obtuse", Spot) != "" { t.Error("Test Failed - ticker PriceToString obtuse value is incorrect") } } func TestGetTicker(t *testing.T) { newPair := pair.NewCurrencyPair("BTC", "USD") - priceStruct := TickerPrice{ + priceStruct := Price{ Pair: newPair, CurrencyPair: newPair.Pair().String(), Last: 1200, @@ -63,21 +63,47 @@ func TestGetTicker(t *testing.T) { PriceATH: 1337, } - bitfinexTicker := CreateNewTicker("bitfinex", newPair, priceStruct) - Tickers = append(Tickers, bitfinexTicker) - - tickerPrice, err := GetTicker("bitfinex", newPair) + ProcessTicker("bitfinex", newPair, priceStruct, Spot) + tickerPrice, err := GetTicker("bitfinex", newPair, Spot) if err != nil { t.Errorf("Test Failed - Ticker GetTicker init error: %s", err) } if tickerPrice.CurrencyPair != "BTCUSD" { t.Error("Test Failed - ticker tickerPrice.CurrencyPair value is incorrect") } + + _, err = GetTicker("blah", newPair, Spot) + if err == nil { + t.Fatal("Test Failed. TestGetTicker returned nil error on invalid exchange") + } + + newPair.FirstCurrency = "ETH" + _, err = GetTicker("bitfinex", newPair, Spot) + if err == nil { + t.Fatal("Test Failed. TestGetTicker returned ticker for invalid first currency") + } + + btcltcPair := pair.NewCurrencyPair("BTC", "LTC") + _, err = GetTicker("bitfinex", btcltcPair, Spot) + if err == nil { + t.Fatal("Test Failed. TestGetTicker returned ticker for invalid second currency") + } + + priceStruct.PriceATH = 9001 + ProcessTicker("bitfinex", newPair, priceStruct, "futures_3m") + tickerPrice, err = GetTicker("bitfinex", newPair, "futures_3m") + if err != nil { + t.Errorf("Test Failed - Ticker GetTicker init error: %s", err) + } + + if tickerPrice.PriceATH != 9001 { + t.Error("Test Failed - ticker tickerPrice.PriceATH value is incorrect") + } } func TestGetTickerByExchange(t *testing.T) { newPair := pair.NewCurrencyPair("BTC", "USD") - priceStruct := TickerPrice{ + priceStruct := Price{ Pair: newPair, CurrencyPair: newPair.Pair().String(), Last: 1200, @@ -89,7 +115,7 @@ func TestGetTickerByExchange(t *testing.T) { PriceATH: 1337, } - anxTicker := CreateNewTicker("ANX", newPair, priceStruct) + anxTicker := CreateNewTicker("ANX", newPair, priceStruct, Spot) Tickers = append(Tickers, anxTicker) tickerPtr, err := GetTickerByExchange("ANX") @@ -103,7 +129,7 @@ func TestGetTickerByExchange(t *testing.T) { func TestFirstCurrencyExists(t *testing.T) { newPair := pair.NewCurrencyPair("BTC", "USD") - priceStruct := TickerPrice{ + priceStruct := Price{ Pair: newPair, CurrencyPair: newPair.Pair().String(), Last: 1200, @@ -115,7 +141,7 @@ func TestFirstCurrencyExists(t *testing.T) { PriceATH: 1337, } - alphaTicker := CreateNewTicker("alphapoint", newPair, priceStruct) + alphaTicker := CreateNewTicker("alphapoint", newPair, priceStruct, Spot) Tickers = append(Tickers, alphaTicker) if !FirstCurrencyExists("alphapoint", "BTC") { @@ -130,7 +156,7 @@ func TestSecondCurrencyExists(t *testing.T) { t.Parallel() newPair := pair.NewCurrencyPair("BTC", "USD") - priceStruct := TickerPrice{ + priceStruct := Price{ Pair: newPair, CurrencyPair: newPair.Pair().String(), Last: 1200, @@ -142,7 +168,7 @@ func TestSecondCurrencyExists(t *testing.T) { PriceATH: 1337, } - bitstampTicker := CreateNewTicker("bitstamp", newPair, priceStruct) + bitstampTicker := CreateNewTicker("bitstamp", newPair, priceStruct, "SPOT") Tickers = append(Tickers, bitstampTicker) if !SecondCurrencyExists("bitstamp", newPair) { @@ -157,7 +183,7 @@ func TestSecondCurrencyExists(t *testing.T) { func TestCreateNewTicker(t *testing.T) { newPair := pair.NewCurrencyPair("BTC", "USD") - priceStruct := TickerPrice{ + priceStruct := Price{ Pair: newPair, CurrencyPair: newPair.Pair().String(), Last: 1200, @@ -169,7 +195,7 @@ func TestCreateNewTicker(t *testing.T) { PriceATH: 1337, } - newTicker := CreateNewTicker("ANX", newPair, priceStruct) + newTicker := CreateNewTicker("ANX", newPair, priceStruct, Spot) if reflect.ValueOf(newTicker).NumField() != 2 { t.Error("Test Failed - ticker CreateNewTicker struct change/or updated") @@ -181,38 +207,39 @@ func TestCreateNewTicker(t *testing.T) { t.Error("Test Failed - ticker CreateNewTicker.ExchangeName value is not ANX") } - if newTicker.Price["BTC"]["USD"].Pair.Pair().String() != "BTCUSD" { + if newTicker.Price["BTC"]["USD"][Spot].Pair.Pair().String() != "BTCUSD" { t.Error("Test Failed - ticker newTicker.Price[BTC][USD].Pair.Pair().String() value is not expected 'BTCUSD'") } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"].Ask).String() != "float64" { + if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].Ask).String() != "float64" { t.Error("Test Failed - ticker newTicker.Price[BTC][USD].Ask value is not a float64") } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"].Bid).String() != "float64" { + if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].Bid).String() != "float64" { t.Error("Test Failed - ticker newTicker.Price[BTC][USD].Bid value is not a float64") } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"].CurrencyPair).String() != "string" { + if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].CurrencyPair).String() != "string" { t.Error("Test Failed - ticker newTicker.Price[BTC][USD].CurrencyPair value is not a string") } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"].High).String() != "float64" { + if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].High).String() != "float64" { t.Error("Test Failed - ticker newTicker.Price[BTC][USD].High value is not a float64") } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"].Last).String() != "float64" { + if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].Last).String() != "float64" { t.Error("Test Failed - ticker newTicker.Price[BTC][USD].Last value is not a float64") } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"].Low).String() != "float64" { + if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].Low).String() != "float64" { t.Error("Test Failed - ticker newTicker.Price[BTC][USD].Low value is not a float64") } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"].PriceATH).String() != "float64" { + if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].PriceATH).String() != "float64" { t.Error("Test Failed - ticker newTicker.Price[BTC][USD].PriceATH value is not a float64") } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"].Volume).String() != "float64" { + if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].Volume).String() != "float64" { t.Error("Test Failed - ticker newTicker.Price[BTC][USD].Volume value is not a float64") } } func TestProcessTicker(t *testing.T) { //non-appending function to tickers + Tickers = []Ticker{} newPair := pair.NewCurrencyPair("BTC", "USD") - priceStruct := TickerPrice{ + priceStruct := Price{ Pair: newPair, CurrencyPair: newPair.Pair().String(), Last: 1200, @@ -224,5 +251,28 @@ func TestProcessTicker(t *testing.T) { //non-appending function to tickers PriceATH: 1337, } - ProcessTicker("btcc", newPair, priceStruct) + ProcessTicker("btcc", newPair, priceStruct, Spot) + + result, err := GetTicker("btcc", newPair, Spot) + if err != nil { + t.Fatal("Test failed. TestProcessTicker failed to create and return a new ticker") + } + + if result.Pair.Pair() != newPair.Pair() { + t.Fatal("Test failed. TestProcessTicker pair mismatch") + } + + secondPair := pair.NewCurrencyPair("BTC", "AUD") + priceStruct.Pair = secondPair + ProcessTicker("btcc", secondPair, priceStruct, Spot) + + result, err = GetTicker("btcc", secondPair, Spot) + if err != nil { + t.Fatal("Test failed. TestProcessTicker failed to create and return a new ticker") + } + + result, err = GetTicker("btcc", newPair, Spot) + if err != nil { + t.Fatal("Test failed. TestProcessTicker failed to return an existing ticker") + } } diff --git a/exchanges/wex/wex.go b/exchanges/wex/wex.go new file mode 100644 index 00000000..ee5ba2fe --- /dev/null +++ b/exchanges/wex/wex.go @@ -0,0 +1,393 @@ +package wex + +import ( + "errors" + "fmt" + "log" + "net/url" + "strconv" + "strings" + "time" + + "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/config" + exchange "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" +) + +const ( + wexAPIPublicURL = "https://wex.nz/api" + wexAPIPrivateURL = "https://wex.nz/tapi" + wexAPIPublicVersion = "3" + wexAPIPrivateVersion = "1" + wexInfo = "info" + wexTicker = "ticker" + wexDepth = "depth" + wexTrades = "trades" + wexAccountInfo = "getInfo" + wexTrade = "Trade" + wexActiveOrders = "ActiveOrders" + wexOrderInfo = "OrderInfo" + wexCancelOrder = "CancelOrder" + wexTradeHistory = "TradeHistory" + wexTransactionHistory = "TransHistory" + wexWithdrawCoin = "WithdrawCoin" + wexCoinDepositAddress = "CoinDepositAddress" + wexCreateCoupon = "CreateCoupon" + wexRedeemCoupon = "RedeemCoupon" +) + +// WEX is the overarching type across the wex package +type WEX struct { + exchange.Base + Ticker map[string]Ticker +} + +// SetDefaults sets current default value for WEX +func (w *WEX) SetDefaults() { + w.Name = "WEX" + w.Enabled = false + w.Fee = 0.2 + w.Verbose = false + w.Websocket = false + w.RESTPollingDelay = 10 + w.Ticker = make(map[string]Ticker) + w.RequestCurrencyPairFormat.Delimiter = "_" + w.RequestCurrencyPairFormat.Uppercase = false + w.RequestCurrencyPairFormat.Separator = "-" + w.ConfigCurrencyPairFormat.Delimiter = "" + w.ConfigCurrencyPairFormat.Uppercase = true + w.AssetTypes = []string{ticker.Spot} +} + +// Setup sets exchange configuration parameters for WEX +func (w *WEX) Setup(exch config.ExchangeConfig) { + if !exch.Enabled { + w.SetEnabled(false) + } else { + w.Enabled = true + w.AuthenticatedAPISupport = exch.AuthenticatedAPISupport + w.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) + w.RESTPollingDelay = exch.RESTPollingDelay + w.Verbose = exch.Verbose + w.Websocket = exch.Websocket + w.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") + w.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") + w.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := w.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = w.SetAssetTypes() + if err != nil { + log.Fatal(err) + } + } +} + +// GetFee returns the exchange fee +func (w *WEX) GetFee() float64 { + return w.Fee +} + +// GetInfo returns the WEX info +func (w *WEX) GetInfo() (Info, error) { + req := fmt.Sprintf("%s/%s/%s/", wexAPIPublicURL, wexAPIPublicVersion, wexInfo) + resp := Info{} + err := common.SendHTTPGetRequest(req, true, w.Verbose, &resp) + + if err != nil { + return resp, err + } + + return resp, nil +} + +// GetTicker returns a ticker for a specific currency +func (w *WEX) GetTicker(symbol string) (map[string]Ticker, error) { + type Response struct { + Data map[string]Ticker + } + + response := Response{} + req := fmt.Sprintf("%s/%s/%s/%s", wexAPIPublicURL, wexAPIPublicVersion, wexTicker, symbol) + err := common.SendHTTPGetRequest(req, true, w.Verbose, &response.Data) + + if err != nil { + return nil, err + } + return response.Data, nil +} + +// GetDepth returns the depth for a specific currency +func (w *WEX) GetDepth(symbol string) (Orderbook, error) { + type Response struct { + Data map[string]Orderbook + } + + response := Response{} + req := fmt.Sprintf("%s/%s/%s/%s", wexAPIPublicURL, wexAPIPublicVersion, wexDepth, symbol) + + err := common.SendHTTPGetRequest(req, true, w.Verbose, &response.Data) + if err != nil { + return Orderbook{}, err + } + + depth := response.Data[symbol] + return depth, nil +} + +// GetTrades returns the trades for a specific currency +func (w *WEX) GetTrades(symbol string) ([]Trades, error) { + type Response struct { + Data map[string][]Trades + } + + response := Response{} + req := fmt.Sprintf("%s/%s/%s/%s", wexAPIPublicURL, wexAPIPublicVersion, wexTrades, symbol) + + err := common.SendHTTPGetRequest(req, true, w.Verbose, &response.Data) + if err != nil { + return nil, err + } + + trades := response.Data[symbol] + return trades, nil +} + +// GetAccountInfo returns a users account info +func (w *WEX) GetAccountInfo() (AccountInfo, error) { + var result AccountInfo + err := w.SendAuthenticatedHTTPRequest(wexAccountInfo, url.Values{}, &result) + + if err != nil { + return result, err + } + + return result, nil +} + +// GetActiveOrders returns the active orders for a specific currency +func (w *WEX) GetActiveOrders(pair string) (map[string]ActiveOrders, error) { + req := url.Values{} + req.Add("pair", pair) + + var result map[string]ActiveOrders + err := w.SendAuthenticatedHTTPRequest(wexActiveOrders, req, &result) + + if err != nil { + return result, err + } + + return result, nil +} + +// GetOrderInfo returns the order info for a specific order ID +func (w *WEX) GetOrderInfo(OrderID int64) (map[string]OrderInfo, error) { + req := url.Values{} + req.Add("order_id", strconv.FormatInt(OrderID, 10)) + + var result map[string]OrderInfo + err := w.SendAuthenticatedHTTPRequest(wexOrderInfo, req, &result) + + if err != nil { + return result, err + } + + return result, nil +} + +// CancelOrder cancels an order for a specific order ID +func (w *WEX) CancelOrder(OrderID int64) (bool, error) { + req := url.Values{} + req.Add("order_id", strconv.FormatInt(OrderID, 10)) + + var result CancelOrder + err := w.SendAuthenticatedHTTPRequest(wexCancelOrder, req, &result) + + if err != nil { + return false, err + } + + return true, nil +} + +// Trade places an order and returns the order ID if successful or an error +func (w *WEX) Trade(pair, orderType string, amount, price float64) (int64, error) { + req := url.Values{} + req.Add("pair", pair) + req.Add("type", orderType) + req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) + req.Add("rate", strconv.FormatFloat(price, 'f', -1, 64)) + + var result Trade + err := w.SendAuthenticatedHTTPRequest(wexTrade, req, &result) + + if err != nil { + return 0, err + } + + return int64(result.OrderID), nil +} + +// GetTransactionHistory returns the transaction history +func (w *WEX) GetTransactionHistory(TIDFrom, Count, TIDEnd int64, order, since, end string) (map[string]TransHistory, error) { + req := url.Values{} + req.Add("from", strconv.FormatInt(TIDFrom, 10)) + req.Add("count", strconv.FormatInt(Count, 10)) + req.Add("from_id", strconv.FormatInt(TIDFrom, 10)) + req.Add("end_id", strconv.FormatInt(TIDEnd, 10)) + req.Add("order", order) + req.Add("since", since) + req.Add("end", end) + + var result map[string]TransHistory + err := w.SendAuthenticatedHTTPRequest(wexTransactionHistory, req, &result) + + if err != nil { + return result, err + } + + return result, nil +} + +// GetTradeHistory returns the trade history +func (w *WEX) GetTradeHistory(TIDFrom, Count, TIDEnd int64, order, since, end, pair string) (map[string]TradeHistory, error) { + req := url.Values{} + req.Add("from", strconv.FormatInt(TIDFrom, 10)) + req.Add("count", strconv.FormatInt(Count, 10)) + req.Add("from_id", strconv.FormatInt(TIDFrom, 10)) + req.Add("end_id", strconv.FormatInt(TIDEnd, 10)) + req.Add("order", order) + req.Add("since", since) + req.Add("end", end) + req.Add("pair", pair) + + var result map[string]TradeHistory + err := w.SendAuthenticatedHTTPRequest(wexTradeHistory, req, &result) + + if err != nil { + return result, err + } + + return result, nil +} + +// WithdrawCoins withdraws coins for a specific coin +func (w *WEX) WithdrawCoins(coin string, amount float64, address string) (WithdrawCoins, error) { + req := url.Values{} + req.Add("coinName", coin) + req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) + req.Add("address", address) + + var result WithdrawCoins + err := w.SendAuthenticatedHTTPRequest(wexWithdrawCoin, req, &result) + + if err != nil { + return result, err + } + return result, nil +} + +// CoinDepositAddress returns the deposit address for a specific currency +func (w *WEX) CoinDepositAddress(coin string) (string, error) { + req := url.Values{} + req.Add("coinName", coin) + + var result CoinDepositAddress + err := w.SendAuthenticatedHTTPRequest(wexCoinDepositAddress, req, &result) + + if err != nil { + return "", nil + } + + return result.Address, nil +} + +// CreateCoupon creates an exchange coupon for a sepcific currency +func (w *WEX) CreateCoupon(currency string, amount float64) (CreateCoupon, error) { + req := url.Values{} + req.Add("currency", currency) + req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) + + var result CreateCoupon + err := w.SendAuthenticatedHTTPRequest(wexCreateCoupon, req, &result) + + if err != nil { + return result, err + } + + return result, nil +} + +// RedeemCoupon redeems an exchange coupon +func (w *WEX) RedeemCoupon(coupon string) (RedeemCoupon, error) { + req := url.Values{} + req.Add("coupon", coupon) + + var result RedeemCoupon + err := w.SendAuthenticatedHTTPRequest(wexRedeemCoupon, req, &result) + + if err != nil { + return result, err + } + + return result, nil +} + +// SendAuthenticatedHTTPRequest sends an authenticated HTTP request to WEX +func (w *WEX) SendAuthenticatedHTTPRequest(method string, values url.Values, result interface{}) (err error) { + if !w.AuthenticatedAPISupport { + return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, w.Name) + } + + if w.Nonce.Get() == 0 { + w.Nonce.Set(time.Now().Unix()) + } else { + w.Nonce.Inc() + } + values.Set("nonce", w.Nonce.String()) + values.Set("method", method) + + encoded := values.Encode() + hmac := common.GetHMAC(common.HashSHA512, []byte(encoded), []byte(w.APISecret)) + + if w.Verbose { + log.Printf("Sending POST request to %s calling method %s with params %s\n", wexAPIPrivateURL, method, encoded) + } + + headers := make(map[string]string) + headers["Key"] = w.APIKey + headers["Sign"] = common.HexEncodeToString(hmac) + headers["Content-Type"] = "application/x-www-form-urlencoded" + + resp, err := common.SendHTTPRequest("POST", wexAPIPrivateURL, headers, strings.NewReader(encoded)) + + if err != nil { + return err + } + + response := Response{} + err = common.JSONDecode([]byte(resp), &response) + + if err != nil { + return err + } + + if response.Success != 1 { + return errors.New(response.Error) + } + + JSONEncoded, err := common.JSONEncode(response.Return) + + if err != nil { + return err + } + + err = common.JSONDecode(JSONEncoded, &result) + + if err != nil { + return err + } + return nil +} diff --git a/exchanges/btce/btce_types.go b/exchanges/wex/wex_types.go similarity index 62% rename from exchanges/btce/btce_types.go rename to exchanges/wex/wex_types.go index a1f8b8e5..a83b654e 100644 --- a/exchanges/btce/btce_types.go +++ b/exchanges/wex/wex_types.go @@ -1,23 +1,26 @@ -package btce +package wex -type BTCeTicker struct { - High float64 - Low float64 - Avg float64 - Vol float64 - Vol_cur float64 - Last float64 - Buy float64 - Sell float64 - Updated int64 +// Ticker stores the ticker information +type Ticker struct { + High float64 + Low float64 + Avg float64 + Vol float64 + VolumeCurrent float64 `json:"vol_cur"` + Last float64 + Buy float64 + Sell float64 + Updated int64 } -type BTCEOrderbook struct { +// Orderbook stores the asks and bids orderbook information +type Orderbook struct { Asks [][]float64 `json:"asks"` Bids [][]float64 `json:"bids"` } -type BTCETrades struct { +// Trades stores trade information +type Trades struct { Type string `json:"type"` Price float64 `json:"bid"` Amount float64 `json:"amount"` @@ -25,13 +28,15 @@ type BTCETrades struct { Timestamp int64 `json:"timestamp"` } -type BTCEResponse struct { +// Response is a generic struct used for exchange API request result +type Response struct { Return interface{} `json:"return"` Success int `json:"success"` Error string `json:"error"` } -type BTCEPair struct { +// Pair holds pair information +type Pair struct { DecimalPlaces int `json:"decimal_places"` MinPrice float64 `json:"min_price"` MaxPrice float64 `json:"max_price"` @@ -40,12 +45,14 @@ type BTCEPair struct { Fee float64 `json:"fee"` } -type BTCEInfo struct { - ServerTime int64 `json:"server_time"` - Pairs map[string]BTCEPair `json:"pairs"` +// Info holds server time and pair information +type Info struct { + ServerTime int64 `json:"server_time"` + Pairs map[string]Pair `json:"pairs"` } -type BTCEAccountInfo struct { +// AccountInfo stores the account information for a user +type AccountInfo struct { Funds map[string]float64 `json:"funds"` OpenOrders int `json:"open_orders"` Rights struct { @@ -57,7 +64,8 @@ type BTCEAccountInfo struct { TransactionCount int `json:"transaction_count"` } -type BTCEActiveOrders struct { +// ActiveOrders stores active order information +type ActiveOrders struct { Pair string `json:"pair"` Type string `json:"sell"` Amount float64 `json:"amount"` @@ -66,7 +74,8 @@ type BTCEActiveOrders struct { Status int `json:"status"` } -type BTCEOrderInfo struct { +// OrderInfo stores order information +type OrderInfo struct { Pair string `json:"pair"` Type string `json:"sell"` StartAmount float64 `json:"start_amount"` @@ -76,19 +85,22 @@ type BTCEOrderInfo struct { Status int `json:"status"` } -type BTCECancelOrder struct { +// CancelOrder is used for the CancelOrder API request response +type CancelOrder struct { OrderID float64 `json:"order_id"` Funds map[string]float64 `json:"funds"` } -type BTCETrade struct { +// Trade stores the trade information +type Trade struct { Received float64 `json:"received"` Remains float64 `json:"remains"` OrderID float64 `json:"order_id"` Funds map[string]float64 `json:"funds"` } -type BTCETransHistory struct { +// TransHistory stores transaction history +type TransHistory struct { Type int `json:"type"` Amount float64 `json:"amount"` Currency string `json:"currency"` @@ -97,7 +109,8 @@ type BTCETransHistory struct { Timestamp float64 `json:"timestamp"` } -type BTCETradeHistory struct { +// TradeHistory stores trade history +type TradeHistory struct { Pair string `json:"pair"` Type string `json:"type"` Amount float64 `json:"amount"` @@ -107,19 +120,27 @@ type BTCETradeHistory struct { Timestamp float64 `json:"timestamp"` } -type BTCEWithdrawCoins struct { +// CoinDepositAddress stores a curency deposit address +type CoinDepositAddress struct { + Address string `json:"address"` +} + +// WithdrawCoins stores information for a withdrawcoins request +type WithdrawCoins struct { TID int64 `json:"tId"` AmountSent float64 `json:"amountSent"` Funds map[string]float64 `json:"funds"` } -type BTCECreateCoupon struct { +// CreateCoupon stores information coupon information +type CreateCoupon struct { Coupon string `json:"coupon"` TransID int64 `json:"transID"` Funds map[string]float64 `json:"funds"` } -type BTCERedeemCoupon struct { +// RedeemCoupon stores redeem coupon information +type RedeemCoupon struct { CouponAmount float64 `json:"couponAmount,string"` CouponCurrency string `json:"couponCurrency"` TransID int64 `json:"transID"` diff --git a/exchanges/wex/wex_wrapper.go b/exchanges/wex/wex_wrapper.go new file mode 100644 index 00000000..67b832e8 --- /dev/null +++ b/exchanges/wex/wex_wrapper.go @@ -0,0 +1,114 @@ +package wex + +import ( + "log" + + "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/currency/pair" + exchange "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" +) + +// Start starts the WEX go routine +func (w *WEX) Start() { + go w.Run() +} + +// Run implements the WEX wrapper +func (w *WEX) Run() { + if w.Verbose { + log.Printf("%s Websocket: %s.", w.GetName(), common.IsEnabled(w.Websocket)) + log.Printf("%s polling delay: %ds.\n", w.GetName(), w.RESTPollingDelay) + log.Printf("%s %d currencies enabled: %s.\n", w.GetName(), len(w.EnabledPairs), w.EnabledPairs) + } +} + +// UpdateTicker updates and returns the ticker for a currency pair +func (w *WEX) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price + pairsCollated, err := exchange.GetAndFormatExchangeCurrencies(w.Name, w.GetEnabledCurrencies()) + if err != nil { + return tickerPrice, err + } + + result, err := w.GetTicker(pairsCollated.String()) + if err != nil { + return tickerPrice, err + } + + for _, x := range w.GetEnabledCurrencies() { + currency := exchange.FormatExchangeCurrency(w.Name, x).Lower().String() + var tp ticker.Price + tp.Pair = x + tp.Last = result[currency].Last + tp.Ask = result[currency].Sell + tp.Bid = result[currency].Buy + tp.Last = result[currency].Last + tp.Low = result[currency].Low + tp.Volume = result[currency].VolumeCurrent + ticker.ProcessTicker(w.Name, x, tp, assetType) + } + return ticker.GetTicker(w.Name, p, assetType) +} + +// GetTickerPrice returns the ticker for a currency pair +func (w *WEX) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tick, err := ticker.GetTicker(w.GetName(), p, assetType) + if err != nil { + return w.UpdateTicker(p, assetType) + } + return tick, nil +} + +// GetOrderbookEx returns the orderbook for a currency pair +func (w *WEX) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(w.GetName(), p, assetType) + if err == nil { + return w.UpdateOrderbook(p, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (w *WEX) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + orderbookNew, err := w.GetDepth(exchange.FormatExchangeCurrency(w.Name, p).String()) + if err != nil { + return orderBook, err + } + + for x := range orderbookNew.Bids { + data := orderbookNew.Bids[x] + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Price: data[0], Amount: data[1]}) + } + + for x := range orderbookNew.Asks { + data := orderbookNew.Asks[x] + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Price: data[0], Amount: data[1]}) + } + + orderbook.ProcessOrderbook(w.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(w.Name, p, assetType) +} + +// GetExchangeAccountInfo retrieves balances for all enabled currencies for the +// WEX exchange +func (w *WEX) GetExchangeAccountInfo() (exchange.AccountInfo, error) { + var response exchange.AccountInfo + response.ExchangeName = w.GetName() + accountBalance, err := w.GetAccountInfo() + if err != nil { + return response, err + } + + for x, y := range accountBalance.Funds { + var exchangeCurrency exchange.AccountCurrencyInfo + exchangeCurrency.CurrencyName = common.StringToUpper(x) + exchangeCurrency.TotalValue = y + exchangeCurrency.Hold = 0 + response.Currencies = append(response.Currencies, exchangeCurrency) + } + + return response, nil +} diff --git a/helpers.go b/helpers.go new file mode 100644 index 00000000..86132f6a --- /dev/null +++ b/helpers.go @@ -0,0 +1,107 @@ +package main + +import ( + "errors" + "fmt" + + "github.com/thrasher-/gocryptotrader/currency/pair" + exchange "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-/gocryptotrader/exchanges/stats" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" +) + +// GetSpecificOrderbook returns a specific orderbook given the currency, +// exchangeName and assetType +func GetSpecificOrderbook(currency, exchangeName, assetType string) (orderbook.Base, error) { + var specificOrderbook orderbook.Base + var err error + for i := 0; i < len(bot.exchanges); i++ { + if bot.exchanges[i] != nil { + if bot.exchanges[i].IsEnabled() && bot.exchanges[i].GetName() == exchangeName { + specificOrderbook, err = bot.exchanges[i].GetOrderbookEx( + pair.NewCurrencyPairFromString(currency), + assetType, + ) + break + } + } + } + return specificOrderbook, err +} + +// GetSpecificTicker returns a specific ticker given the currency, +// exchangeName and assetType +func GetSpecificTicker(currency, exchangeName, assetType string) (ticker.Price, error) { + var specificTicker ticker.Price + var err error + for i := 0; i < len(bot.exchanges); i++ { + if bot.exchanges[i] != nil { + if bot.exchanges[i].IsEnabled() && bot.exchanges[i].GetName() == exchangeName { + specificTicker, err = bot.exchanges[i].GetTickerPrice( + pair.NewCurrencyPairFromString(currency), + assetType, + ) + break + } + } + } + return specificTicker, err +} + +// GetCollatedExchangeAccountInfoByCoin collates individual exchange account +// information and turns into into a map string of +// exchange.AccountCurrencyInfo +func GetCollatedExchangeAccountInfoByCoin(accounts []exchange.AccountInfo) map[string]exchange.AccountCurrencyInfo { + result := make(map[string]exchange.AccountCurrencyInfo) + for i := 0; i < len(accounts); i++ { + for j := 0; j < len(accounts[i].Currencies); j++ { + currencyName := accounts[i].Currencies[j].CurrencyName + avail := accounts[i].Currencies[j].TotalValue + onHold := accounts[i].Currencies[j].Hold + + info, ok := result[currencyName] + if !ok { + accountInfo := exchange.AccountCurrencyInfo{CurrencyName: currencyName, Hold: onHold, TotalValue: avail} + result[currencyName] = accountInfo + } else { + info.Hold += onHold + info.TotalValue += avail + result[currencyName] = info + } + } + } + return result +} + +// GetAccountCurrencyInfoByExchangeName returns info for an exchange +func GetAccountCurrencyInfoByExchangeName(accounts []exchange.AccountInfo, exchangeName string) (exchange.AccountInfo, error) { + for i := 0; i < len(accounts); i++ { + if accounts[i].ExchangeName == exchangeName { + return accounts[i], nil + } + } + return exchange.AccountInfo{}, errors.New(exchange.ErrExchangeNotFound) +} + +// GetExchangeHighestPriceByCurrencyPair returns the exchange with the highest +// price for a given currency pair and asset type +func GetExchangeHighestPriceByCurrencyPair(p pair.CurrencyPair, assetType string) (string, error) { + result := stats.SortExchangesByPrice(p, assetType, true) + if len(result) != 1 { + return "", fmt.Errorf("no stats for supplied currency pair and asset type") + } + + return result[0].Exchange, nil +} + +// GetExchangeLowestPriceByCurrencyPair returns the exchange with the lowest +// price for a given currency pair and asset type +func GetExchangeLowestPriceByCurrencyPair(p pair.CurrencyPair, assetType string) (string, error) { + result := stats.SortExchangesByPrice(p, assetType, false) + if len(result) != 1 { + return "", fmt.Errorf("no stats for supplied currency pair and asset type") + } + + return result[0].Exchange, nil +} diff --git a/main.go b/main.go index d7a1e47c..7d6dc1a5 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,6 @@ import ( "github.com/thrasher-/gocryptotrader/exchanges/bitstamp" "github.com/thrasher-/gocryptotrader/exchanges/bittrex" "github.com/thrasher-/gocryptotrader/exchanges/btcc" - "github.com/thrasher-/gocryptotrader/exchanges/btce" "github.com/thrasher-/gocryptotrader/exchanges/btcmarkets" "github.com/thrasher-/gocryptotrader/exchanges/coinut" "github.com/thrasher-/gocryptotrader/exchanges/gdax" @@ -33,6 +32,7 @@ import ( "github.com/thrasher-/gocryptotrader/exchanges/okcoin" "github.com/thrasher-/gocryptotrader/exchanges/poloniex" "github.com/thrasher-/gocryptotrader/exchanges/ticker" + "github.com/thrasher-/gocryptotrader/exchanges/wex" "github.com/thrasher-/gocryptotrader/portfolio" "github.com/thrasher-/gocryptotrader/smsglobal" ) @@ -44,7 +44,7 @@ type ExchangeMain struct { bitstamp bitstamp.Bitstamp bitfinex bitfinex.Bitfinex bittrex bittrex.Bittrex - btce btce.BTCE + wex wex.WEX btcmarkets btcmarkets.BTCMarkets coinut coinut.COINUT gdax gdax.GDAX @@ -64,6 +64,7 @@ type ExchangeMain struct { // overarching type across this code base. type Bot struct { config *config.Config + smsglobal *smsglobal.Base portfolio *portfolio.Base exchange ExchangeMain exchanges []exchange.IBotExchange @@ -115,20 +116,17 @@ func main() { log.Fatal(err) } - log.Printf("Bot '%s' started.\n", bot.config.Name) AdjustGoMaxProcs() + log.Printf("Bot '%s' started.\n", bot.config.Name) + log.Printf("Fiat display currency: %s.", bot.config.FiatDisplayCurrency) if bot.config.SMS.Enabled { - err = bot.config.CheckSMSGlobalConfigValues() - if err != nil { - log.Println(err) // non fatal event - bot.config.SMS.Enabled = false - } else { - log.Printf( - "SMS support enabled. Number of SMS contacts %d.\n", - smsglobal.GetEnabledSMSContacts(bot.config.SMS), - ) - } + bot.smsglobal = smsglobal.New(bot.config.SMS.Username, bot.config.SMS.Password, + bot.config.Name, bot.config.SMS.Contacts) + log.Printf( + "SMS support enabled. Number of SMS contacts %d.\n", + bot.smsglobal.GetEnabledContacts(), + ) } else { log.Println("SMS support disabled.") } @@ -146,7 +144,7 @@ func main() { new(bitstamp.Bitstamp), new(bitfinex.Bitfinex), new(bittrex.Bittrex), - new(btce.BTCE), + new(wex.WEX), new(btcmarkets.BTCMarkets), new(coinut.COINUT), new(gdax.GDAX), @@ -173,11 +171,24 @@ func main() { setupBotExchanges() - bot.config.RetrieveConfigCurrencyPairs() + if bot.config.CurrencyExchangeProvider == "yahoo" { + currency.SetProvider(true) + } else { + currency.SetProvider(false) + } + log.Printf("Using %s as currency exchange provider.", bot.config.CurrencyExchangeProvider) + + bot.config.RetrieveConfigCurrencyPairs() err = currency.SeedCurrencyData(currency.BaseCurrencies) if err != nil { - log.Fatalf("Fatal error retrieving config currencies. Error: %s", err) + currency.SwapProvider() + log.Printf("'%s' currency exchange provider failed, swapping to %s and testing..", + bot.config.CurrencyExchangeProvider, currency.GetProvider()) + err = currency.SeedCurrencyData(currency.BaseCurrencies) + if err != nil { + log.Fatalf("Fatal error retrieving config currencies. Error: %s", err) + } } log.Println("Successfully retrieved config currencies.") @@ -187,23 +198,22 @@ func main() { SeedExchangeAccountInfo(GetAllEnabledExchangeAccountInfo().Data) go portfolio.StartPortfolioWatcher() + log.Println("Starting websocket handler") + go WebsocketHandler() + + go TickerUpdaterRoutine() + go OrderbookUpdaterRoutine() + if bot.config.Webserver.Enabled { - err := bot.config.CheckWebserverConfigValues() - if err != nil { - log.Println(err) // non fatal event - //bot.config.Webserver.Enabled = false - } else { - listenAddr := bot.config.Webserver.ListenAddress - log.Printf( - "HTTP Webserver support enabled. Listen URL: http://%s:%d/\n", - common.ExtractHost(listenAddr), common.ExtractPort(listenAddr), - ) - router := NewRouter(bot.exchanges) - log.Fatal(http.ListenAndServe(listenAddr, router)) - } - } - if !bot.config.Webserver.Enabled { - log.Println("HTTP Webserver support disabled.") + listenAddr := bot.config.Webserver.ListenAddress + log.Printf( + "HTTP Webserver support enabled. Listen URL: http://%s:%d/\n", + common.ExtractHost(listenAddr), common.ExtractPort(listenAddr), + ) + router := NewRouter(bot.exchanges) + log.Fatal(http.ListenAndServe(listenAddr, router)) + } else { + log.Println("HTTP RESTful Webserver support disabled.") } <-bot.shutdown @@ -293,9 +303,15 @@ func SeedExchangeAccountInfo(data []exchange.AccountInfo) { currencyName) port.RemoveExchangeAddress(exchangeName, currencyName) } else { - log.Printf("Portfolio: Updating %s %s entry with balance %f.\n", - exchangeName, currencyName, total) - port.UpdateExchangeAddressBalance(exchangeName, currencyName, total) + balance, ok := port.GetAddressBalance(exchangeName, currencyName, portfolio.PortfolioAddressExchange) + if !ok { + continue + } + if balance != total { + log.Printf("Portfolio: Updating %s %s entry with balance %f.\n", + exchangeName, currencyName, total) + port.UpdateExchangeAddressBalance(exchangeName, currencyName, total) + } } } } diff --git a/portfolio/portfolio.go b/portfolio/portfolio.go index ddff2170..ff486a79 100644 --- a/portfolio/portfolio.go +++ b/portfolio/portfolio.go @@ -10,9 +10,7 @@ import ( ) const ( - blockrAPIURL = "blockr.io/api" - blockrAPIVersion = "1" - blockrAddressBalance = "address/balance" + cryptoIDAPIURL = "https://chainz.cryptoid.info" etherchainAPIURL = "https://etherchain.org/api" etherchainAccountMultiple = "account/multiple" @@ -38,32 +36,6 @@ type Address struct { Description string } -// BlockrAddress holds JSON incoming and outgoing data for BLOCKR with address -// information -type BlockrAddress struct { - Address string `json:"address"` - Balance float64 `json:"balance"` - BalanceMultisig float64 `json:"balance_multisig"` -} - -// BlockrAddressBalanceSingle holds JSON incoming and outgoing data for BLOCKR -// with address balance information -type BlockrAddressBalanceSingle struct { - Status string `json:"status"` - Data BlockrAddress `json:"data"` - Code int `json:"code"` - Message string `json:"message"` -} - -// BlockrAddressBalanceMulti holds JSON incoming and outgoing data for BLOCKR -// with address balance information for multiple wallets -type BlockrAddressBalanceMulti struct { - Status string `json:"status"` - Data []BlockrAddress `json:"data"` - Code int `json:"code"` - Message string `json:"message"` -} - // EtherchainBalanceResponse holds JSON incoming and outgoing data for // Etherchain type EtherchainBalanceResponse struct { @@ -108,7 +80,7 @@ func GetEthereumBalance(address []string) (EtherchainBalanceResponse, error) { "%s/%s/%s", etherchainAPIURL, etherchainAccountMultiple, addresses, ) result := EtherchainBalanceResponse{} - err := common.SendHTTPGetRequest(url, true, &result) + err := common.SendHTTPGetRequest(url, true, false, &result) if err != nil { return result, err } @@ -118,63 +90,30 @@ func GetEthereumBalance(address []string) (EtherchainBalanceResponse, error) { return result, nil } -// GetBlockrBalanceSingle queries Blockr for an address balance for either a -// LTC or a BTC single address -func GetBlockrBalanceSingle(address string, coinType string) (BlockrAddressBalanceSingle, error) { - valid, _ := common.IsValidCryptoAddress(address, coinType) - if !valid { - return BlockrAddressBalanceSingle{}, fmt.Errorf( - "Not a %s address", common.StringToUpper(coinType), - ) +// GetCryptoIDAddress queries CryptoID for an address balance for a +// specified cryptocurrency +func GetCryptoIDAddress(address string, coinType string) (float64, error) { + ok, err := common.IsValidCryptoAddress(address, coinType) + if !ok || err != nil { + return 0, errors.New("invalid address") } - url := fmt.Sprintf( - "https://%s.%s/v%s/%s/%s", common.StringToLower(coinType), blockrAPIURL, - blockrAPIVersion, blockrAddressBalance, address, - ) - result := BlockrAddressBalanceSingle{} - err := common.SendHTTPGetRequest(url, true, &result) + var result interface{} + url := fmt.Sprintf("%s/%s/api.dws?q=getbalance&a=%s", cryptoIDAPIURL, common.StringToLower(coinType), address) + err = common.SendHTTPGetRequest(url, true, false, &result) if err != nil { - return result, err + return 0, err } - if result.Status != "success" { - return result, errors.New(result.Message) - } - return result, nil -} - -// GetBlockrAddressMulti queries Blockr for an address balance for either a LTC -// or a BTC multiple addresses -func GetBlockrAddressMulti(addresses []string, coinType string) (BlockrAddressBalanceMulti, error) { - for _, add := range addresses { - valid, _ := common.IsValidCryptoAddress(add, coinType) - if !valid { - return BlockrAddressBalanceMulti{}, fmt.Errorf( - "Not a %s address", common.StringToUpper(coinType), - ) - } - } - addressesStr := common.JoinStrings(addresses, ",") - url := fmt.Sprintf( - "https://%s.%s/v%s/%s/%s", common.StringToLower(coinType), blockrAPIURL, - blockrAPIVersion, blockrAddressBalance, addressesStr, - ) - result := BlockrAddressBalanceMulti{} - err := common.SendHTTPGetRequest(url, true, &result) - if err != nil { - return result, err - } - if result.Status != "success" { - return result, errors.New(result.Message) - } - return result, nil + return result.(float64), nil } // GetAddressBalance acceses the portfolio base and returns the balance by passed -// in address -func (p *Base) GetAddressBalance(address string) (float64, bool) { +// in address, coin type and description +func (p *Base) GetAddressBalance(address, coinType, description string) (float64, bool) { for x := range p.Addresses { - if p.Addresses[x].Address == address { + if p.Addresses[x].Address == address && + p.Addresses[x].Description == description && + p.Addresses[x].CoinType == coinType { return p.Addresses[x].Balance, true } } @@ -213,6 +152,18 @@ func (p *Base) ExchangeAddressExists(exchangeName, coinType string) bool { return false } +// AddExchangeAddress adds an exchange address to the portfolio base +func (p *Base) AddExchangeAddress(exchangeName, coinType string, balance float64) { + if p.ExchangeAddressExists(exchangeName, coinType) { + p.UpdateExchangeAddressBalance(exchangeName, coinType, balance) + } else { + p.Addresses = append( + p.Addresses, Address{Address: exchangeName, CoinType: coinType, + Balance: balance, Description: PortfolioAddressExchange}, + ) + } +} + // UpdateAddressBalance updates the portfolio base balance func (p *Base) UpdateAddressBalance(address string, amount float64) { for x := range p.Addresses { @@ -244,6 +195,10 @@ func (p *Base) UpdateExchangeAddressBalance(exchangeName, coinType string, balan // AddAddress adds an address to the portfolio base func (p *Base) AddAddress(address, coinType, description string, balance float64) { + if description == PortfolioAddressExchange { + p.AddExchangeAddress(address, coinType, balance) + return + } if !p.AddressExists(address) { p.Addresses = append( p.Addresses, Address{Address: address, CoinType: coinType, @@ -286,22 +241,12 @@ func (p *Base) UpdatePortfolio(addresses []string, coinType string) bool { } return true } - if len(addresses) > 1 { - result, err := GetBlockrAddressMulti(addresses, coinType) + for x := range addresses { + result, err := GetCryptoIDAddress(addresses[x], coinType) if err != nil { return false } - for _, x := range result.Data { - p.AddAddress(x.Address, coinType, PortfolioAddressPersonal, x.Balance) - } - } else { - result, err := GetBlockrBalanceSingle(addresses[0], coinType) - if err != nil { - return false - } - p.AddAddress( - addresses[0], coinType, PortfolioAddressPersonal, result.Data.Balance, - ) + p.AddAddress(addresses[x], coinType, PortfolioAddressPersonal, result) } return true } @@ -413,12 +358,7 @@ func (p *Base) GetPortfolioSummary() Summary { y = y / common.WeiPerEther personalHoldings[x] = y } - balance, ok := totalCoins[x] - if !ok { - totalCoins[x] = y - } else { - totalCoins[x] = y + balance - } + totalCoins[x] = y } for x, y := range exchangeHoldings { diff --git a/portfolio/portfolio_test.go b/portfolio/portfolio_test.go index 3a966481..7a3a9c94 100644 --- a/portfolio/portfolio_test.go +++ b/portfolio/portfolio_test.go @@ -32,62 +32,11 @@ func TestGetEthereumBalance(t *testing.T) { } } -func TestGetBlockrBalanceSingle(t *testing.T) { - litecoinAddress := "LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL" - bitcoinAddress := "3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r" - nonsenseAddress := "DingDong" - ltc := "LtC" - btc := "bTc" - - response, err := GetBlockrBalanceSingle(litecoinAddress, ltc) +func TestGetCryptoIDBalance(t *testing.T) { + ltcAddress := "LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1" + _, err := GetCryptoIDAddress(ltcAddress, "ltc") if err != nil { - t.Errorf("Test Failed - Portfolio GetBlockrBalanceSingle() Error: %s", err) - } - response, err = GetBlockrBalanceSingle(litecoinAddress, btc) - if err == nil { - t.Errorf("Test Failed - Portfolio GetBlockrBalanceSingle() Error: %s", err) - } - response, err = GetBlockrBalanceSingle(bitcoinAddress, btc) - if err != nil { - t.Errorf("Test Failed - Portfolio GetBlockrBalanceSingle() Error: %s", err) - } - response, err = GetBlockrBalanceSingle(bitcoinAddress, ltc) - if err != nil { - t.Errorf("Test Failed - Portfolio GetBlockrBalanceSingle() Error: %s", err) - } - response, err = GetBlockrBalanceSingle(nonsenseAddress, ltc+btc) - if err == nil { - t.Errorf("Test Failed - Portfolio GetBlockrBalanceSingle() Error: %s", err) - } - if response.Status == "success" { - t.Error( - "Test Failed - Portfolio GetBlockrBalanceSingle() Error: Incorrect status", - ) - } -} - -func TestGetBlockrAddressMulti(t *testing.T) { - litecoinAddresses := []string{ - "LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL", "LVa8wZ983PvWtdwXZ8viK6SocMENLCXkEy", - } - bitcoinAddresses := []string{ - "3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r", "3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v", - } - nonsenseAddresses := []string{"DingDong", "ningNang"} - ltc := "LtC" - btc := "bTc" - - _, err := GetBlockrAddressMulti(litecoinAddresses, ltc) - if err != nil { - t.Errorf("Test Failed - Portfolio GetBlockrAddressMulti() Error: %s", err) - } - _, err = GetBlockrAddressMulti(bitcoinAddresses, btc) - if err != nil { - t.Errorf("Test Failed - Portfolio GetBlockrAddressMulti() Error: %s", err) - } - _, err = GetBlockrAddressMulti(nonsenseAddresses, ltc) - if err == nil { - t.Errorf("Test Failed - Portfolio GetBlockrAddressMulti() Error") + t.Fatalf("Test failed. TestGetCryptoIDBalance error: %s", err) } } @@ -100,12 +49,12 @@ func TestGetAddressBalance(t *testing.T) { portfolio := Base{} portfolio.AddAddress(ltcAddress, ltc, description, balance) - addBalance, _ := portfolio.GetAddressBalance("LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL") + addBalance, _ := portfolio.GetAddressBalance("LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL", ltc, description) if addBalance != balance { t.Error("Test Failed - Portfolio GetAddressBalance() Error: Incorrect value") } - addBalance, found := portfolio.GetAddressBalance("WigWham") + addBalance, found := portfolio.GetAddressBalance("WigWham", ltc, description) if addBalance != 0 { t.Error("Test Failed - Portfolio GetAddressBalance() Error: Incorrect value") } @@ -148,6 +97,16 @@ func TestExchangeAddressExists(t *testing.T) { } +func TestAddExchangeAddress(t *testing.T) { + newbase := Base{} + newbase.AddExchangeAddress("ANX", "BTC", 100) + newbase.AddExchangeAddress("ANX", "BTC", 200) + + if !newbase.ExchangeAddressExists("ANX", "BTC") { + t.Error("Test Failed - TestExchangeAddressExists address doesn't exist") + } +} + func TestUpdateAddressBalance(t *testing.T) { newbase := Base{} newbase.AddAddress("someaddress", "LTC", "LTCWALLETTEST", 0.02) @@ -178,7 +137,7 @@ func TestRemoveExchangeAddress(t *testing.T) { exchangeName := "BallerExchange" coinType := "LTC" - newbase.AddAddress(exchangeName, coinType, PortfolioAddressExchange, 420) + newbase.AddExchangeAddress(exchangeName, coinType, 420) if !newbase.ExchangeAddressExists(exchangeName, coinType) { t.Error("Test failed - portfolio_test.go - TestRemoveAddress") @@ -192,7 +151,7 @@ func TestRemoveExchangeAddress(t *testing.T) { func TestUpdateExchangeAddressBalance(t *testing.T) { newbase := Base{} - newbase.AddAddress("someaddress", "LTC", "LTCWALLETTEST", 0.02) + newbase.AddExchangeAddress("someaddress", "LTC", 0.02) portfolio := GetPortfolio() portfolio.SeedPortfolio(newbase) portfolio.UpdateExchangeAddressBalance("someaddress", "LTC", 0.04) @@ -273,8 +232,8 @@ func TestUpdatePortfolio(t *testing.T) { func TestGetPortfolioByExchange(t *testing.T) { newbase := Base{} - newbase.AddAddress("ANX", "LTC", PortfolioAddressExchange, 0.07) - newbase.AddAddress("Bitfinex", "LTC", PortfolioAddressExchange, 0.05) + newbase.AddExchangeAddress("ANX", "LTC", 0.07) + newbase.AddExchangeAddress("Bitfinex", "LTC", 0.05) newbase.AddAddress("someaddress", "LTC", PortfolioAddressPersonal, 0.03) portfolio := GetPortfolio() portfolio.SeedPortfolio(newbase) @@ -340,15 +299,17 @@ func TestGetPortfolioSummary(t *testing.T) { newbase := Base{} // Personal holdings newbase.AddAddress("someaddress", "LTC", PortfolioAddressPersonal, 1) + newbase.AddAddress("someaddress2", "LTC", PortfolioAddressPersonal, 2) + newbase.AddAddress("someaddress3", "BTC", PortfolioAddressPersonal, 100) newbase.AddAddress("0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", "ETH", PortfolioAddressPersonal, 865346880000000000) newbase.AddAddress("0x9edc81c813b26165f607a8d1b8db87a02f34307f", "ETH", PortfolioAddressPersonal, 165346880000000000) // Exchange holdings - newbase.AddAddress("Bitfinex", "LTC", PortfolioAddressExchange, 20) - newbase.AddAddress("Bitfinex", "BTC", PortfolioAddressExchange, 100) - newbase.AddAddress("ANX", "ETH", PortfolioAddressExchange, 42) + newbase.AddExchangeAddress("Bitfinex", "LTC", 20) + newbase.AddExchangeAddress("Bitfinex", "BTC", 100) + newbase.AddExchangeAddress("ANX", "ETH", 42) portfolio := GetPortfolio() portfolio.SeedPortfolio(newbase) @@ -371,7 +332,11 @@ func TestGetPortfolioSummary(t *testing.T) { t.Error("Test Failed - portfolio_test.go - TestGetPortfolioSummary error") } - if getTotalsVal("LTC").Balance != 101 { + if getTotalsVal("LTC").Balance != 23 { + t.Error("Test Failed - portfolio_test.go - TestGetPortfolioSummary error") + } + + if getTotalsVal("BTC").Balance != 200 { t.Error("Test Failed - portfolio_test.go - TestGetPortfolioSummary error") } } @@ -400,7 +365,17 @@ func TestSeedPortfolio(t *testing.T) { } func TestStartPortfolioWatcher(t *testing.T) { - //Not until testTimeoutFeature and errors + newBase := Base{} + newBase.AddAddress("LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1", "LTC", PortfolioAddressPersonal, 0.02) + newBase.AddAddress("Testy", "LTC", PortfolioAddressPersonal, 0.02) + portfolio := GetPortfolio() + portfolio.SeedPortfolio(newBase) + + if !portfolio.AddressExists("LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1") { + t.Error("Test Failed - portfolio_test.go - TestStartPortfolioWatcher") + } + + go StartPortfolioWatcher() } func TestGetPortfolio(t *testing.T) { diff --git a/portfolio_routes.go b/portfolio_routes.go deleted file mode 100644 index d2117160..00000000 --- a/portfolio_routes.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "encoding/json" - "net/http" -) - -// RESTGetPortfolio replies to a request with an encoded JSON response of the -// portfolio -func RESTGetPortfolio(w http.ResponseWriter, r *http.Request) { - result := bot.portfolio.GetPortfolioSummary() - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(result); err != nil { - panic(err) - } -} - -// PortfolioRoutes declares the current routes for config_routes.go -var PortfolioRoutes = Routes{ - Route{ - "GetPortfolio", - "GET", - "/portfolio/all", - RESTGetPortfolio, - }, -} diff --git a/restful_logger.go b/restful_logger.go deleted file mode 100644 index 2025c5a3..00000000 --- a/restful_logger.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "log" - "net/http" - "time" -) - -// Logger logs the requests internally -func Logger(inner http.Handler, name string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - - inner.ServeHTTP(w, r) - - log.Printf( - "%s\t%s\t%s\t%s", - r.Method, - r.RequestURI, - name, - time.Since(start), - ) - }) -} diff --git a/restful_router.go b/restful_router.go index 350ba9c9..f6666bac 100644 --- a/restful_router.go +++ b/restful_router.go @@ -1,24 +1,117 @@ package main import ( + "fmt" + "log" "net/http" + "time" "github.com/gorilla/mux" "github.com/thrasher-/gocryptotrader/exchanges" ) +// RESTLogger logs the requests internally +func RESTLogger(inner http.Handler, name string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + inner.ServeHTTP(w, r) + + log.Printf( + "%s\t%s\t%s\t%s", + r.Method, + r.RequestURI, + name, + time.Since(start), + ) + }) +} + +// Route is a sub type that holds the request routes +type Route struct { + Name string + Method string + Pattern string + HandlerFunc http.HandlerFunc +} + +// Routes is an array of all the registered routes +type Routes []Route + +var routes = Routes{} + // NewRouter takes in the exchange interfaces and returns a new multiplexor // router func NewRouter(exchanges []exchange.IBotExchange) *mux.Router { router := mux.NewRouter().StrictSlash(true) - allRoutes := append(routes, ExchangeRoutes...) - allRoutes = append(allRoutes, ConfigRoutes...) - allRoutes = append(allRoutes, PortfolioRoutes...) - allRoutes = append(allRoutes, WalletRoutes...) - for _, route := range allRoutes { + + routes = Routes{ + Route{ + "", + "GET", + "/", + getIndex, + }, + Route{ + "GetAllSettings", + "GET", + "/config/all", + RESTGetAllSettings, + }, + Route{ + "SaveAllSettings", + "POST", + "/config/all/save", + RESTSaveAllSettings, + }, + Route{ + "AllEnabledAccountInfo", + "GET", + "/exchanges/enabled/accounts/all", + RESTGetAllEnabledAccountInfo, + }, + Route{ + "AllActiveExchangesAndCurrencies", + "GET", + "/exchanges/enabled/latest/all", + RESTGetAllActiveTickers, + }, + Route{ + "IndividualExchangeAndCurrency", + "GET", + "/exchanges/{exchangeName}/latest/{currency}", + RESTGetTicker, + }, + Route{ + "GetPortfolio", + "GET", + "/portfolio/all", + RESTGetPortfolio, + }, + Route{ + "AllActiveExchangesAndOrderbooks", + "GET", + "/exchanges/orderbook/latest/all", + RESTGetAllActiveOrderbooks, + }, + Route{ + "IndividualExchangeOrderbook", + "GET", + "/exchanges/{exchangeName}/orderbook/latest/{currency}", + RESTGetOrderbook, + }, + Route{ + "ws", + "GET", + "/ws", + WebsocketClientHandler, + }, + } + + for _, route := range routes { var handler http.Handler handler = route.HandlerFunc - handler = Logger(handler, route.Name) + handler = RESTLogger(handler, route.Name) router. Methods(route.Method). @@ -28,3 +121,8 @@ func NewRouter(exchanges []exchange.IBotExchange) *mux.Router { } return router } + +func getIndex(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "GoCryptoTrader RESTful interface. For the web GUI, please visit the web GUI readme.") + w.WriteHeader(http.StatusOK) +} diff --git a/restful_router_test.go b/restful_router_test.go deleted file mode 100644 index e7fd9cf8..00000000 --- a/restful_router_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - "testing" -) - -func TestNewRouter(t *testing.T) { - if value := NewRouter(bot.exchanges); value.KeepContext { - t.Error("Test Failed - Restful_Router_Test.go - NewRouter Error") - } -} diff --git a/restful_routes.go b/restful_routes.go deleted file mode 100644 index 4728f9be..00000000 --- a/restful_routes.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import "net/http" - -// Route is a sub type that holds the request routes -type Route struct { - Name string - Method string - Pattern string - HandlerFunc http.HandlerFunc -} - -// Routes is an array of all the registered routes -type Routes []Route - -var routes = Routes{} diff --git a/restful_server.go b/restful_server.go new file mode 100644 index 00000000..f5103227 --- /dev/null +++ b/restful_server.go @@ -0,0 +1,297 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/thrasher-/gocryptotrader/config" + exchange "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" +) + +// AllEnabledExchangeOrderbooks holds the enabled exchange orderbooks +type AllEnabledExchangeOrderbooks struct { + Data []EnabledExchangeOrderbooks `json:"data"` +} + +// EnabledExchangeOrderbooks is a sub type for singular exchanges and respective +// orderbooks +type EnabledExchangeOrderbooks struct { + ExchangeName string `json:"exchangeName"` + ExchangeValues []orderbook.Base `json:"exchangeValues"` +} + +// AllEnabledExchangeCurrencies holds the enabled exchange currencies +type AllEnabledExchangeCurrencies struct { + Data []EnabledExchangeCurrencies `json:"data"` +} + +// EnabledExchangeCurrencies is a sub type for singular exchanges and respective +// currencies +type EnabledExchangeCurrencies struct { + ExchangeName string `json:"exchangeName"` + ExchangeValues []ticker.Price `json:"exchangeValues"` +} + +// AllEnabledExchangeAccounts holds all enabled accounts info +type AllEnabledExchangeAccounts struct { + Data []exchange.AccountInfo `json:"data"` +} + +// RESTfulJSONResponse outputs a JSON response of the req interface +func RESTfulJSONResponse(w http.ResponseWriter, r *http.Request, req interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(req); err != nil { + return err + } + return nil +} + +// RESTfulError prints the REST method and error +func RESTfulError(method string, err error) { + log.Printf("RESTful %s: server failed to send JSON response. Error %s", + method, err) +} + +// RESTGetAllSettings replies to a request with an encoded JSON response about the +// trading bots configuration. +func RESTGetAllSettings(w http.ResponseWriter, r *http.Request) { + err := RESTfulJSONResponse(w, r, bot.config) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// RESTSaveAllSettings saves all current settings from request body as a JSON +// document then reloads state and returns the settings +func RESTSaveAllSettings(w http.ResponseWriter, r *http.Request) { + //Get the data from the request + decoder := json.NewDecoder(r.Body) + var responseData config.Post + err := decoder.Decode(&responseData) + if err != nil { + RESTfulError(r.Method, err) + } + //Save change the settings + err = bot.config.UpdateConfig(bot.configFile, responseData.Data) + if err != nil { + RESTfulError(r.Method, err) + } + + err = RESTfulJSONResponse(w, r, bot.config) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// RESTGetOrderbook returns orderbook info for a given currency, exchange and +// asset type +func RESTGetOrderbook(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + currency := vars["currency"] + exchange := vars["exchangeName"] + assetType := vars["assetType"] + + if assetType == "" { + assetType = orderbook.Spot + } + + response, err := GetSpecificOrderbook(currency, exchange, assetType) + if err != nil { + log.Printf("Failed to fetch orderbook for %s currency: %s\n", exchange, + currency) + return + } + + err = RESTfulJSONResponse(w, r, response) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// GetAllActiveOrderbooks returns all enabled exchanges orderbooks +func GetAllActiveOrderbooks() []EnabledExchangeOrderbooks { + var orderbookData []EnabledExchangeOrderbooks + + for _, individualBot := range bot.exchanges { + if individualBot != nil && individualBot.IsEnabled() { + var individualExchange EnabledExchangeOrderbooks + exchangeName := individualBot.GetName() + individualExchange.ExchangeName = exchangeName + currencies := individualBot.GetEnabledCurrencies() + assetTypes, err := exchange.GetExchangeAssetTypes(exchangeName) + if err != nil { + log.Printf("failed to get %s exchange asset types. Error: %s", + exchangeName, err) + continue + } + for _, x := range currencies { + currency := x + + var ob orderbook.Base + if len(assetTypes) > 1 { + for y := range assetTypes { + ob, err = individualBot.GetOrderbookEx(currency, + assetTypes[y]) + } + } else { + ob, err = individualBot.GetOrderbookEx(currency, + assetTypes[0]) + } + + if err != nil { + log.Printf("failed to get %s %s orderbook. Error: %s", + currency.Pair().String(), + exchangeName, + err) + continue + } + + individualExchange.ExchangeValues = append( + individualExchange.ExchangeValues, ob, + ) + } + orderbookData = append(orderbookData, individualExchange) + } + } + return orderbookData +} + +// RESTGetAllActiveOrderbooks returns all enabled exchange orderbooks +func RESTGetAllActiveOrderbooks(w http.ResponseWriter, r *http.Request) { + var response AllEnabledExchangeOrderbooks + response.Data = GetAllActiveOrderbooks() + + err := RESTfulJSONResponse(w, r, response) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// RESTGetPortfolio returns the bot portfolio +func RESTGetPortfolio(w http.ResponseWriter, r *http.Request) { + result := bot.portfolio.GetPortfolioSummary() + err := RESTfulJSONResponse(w, r, result) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// RESTGetTicker returns ticker info for a given currency, exchange and +// asset type +func RESTGetTicker(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + currency := vars["currency"] + exchange := vars["exchangeName"] + assetType := vars["assetType"] + + if assetType == "" { + assetType = ticker.Spot + } + response, err := GetSpecificTicker(currency, exchange, assetType) + if err != nil { + log.Printf("Failed to fetch ticker for %s currency: %s\n", exchange, + currency) + return + } + err = RESTfulJSONResponse(w, r, response) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// GetAllActiveTickers returns all enabled exchange tickers +func GetAllActiveTickers() []EnabledExchangeCurrencies { + var tickerData []EnabledExchangeCurrencies + + for _, individualBot := range bot.exchanges { + if individualBot != nil && individualBot.IsEnabled() { + var individualExchange EnabledExchangeCurrencies + exchangeName := individualBot.GetName() + individualExchange.ExchangeName = exchangeName + log.Println( + "Getting enabled currencies for '" + exchangeName + "'", + ) + currencies := individualBot.GetEnabledCurrencies() + for _, x := range currencies { + currency := x + assetTypes, err := exchange.GetExchangeAssetTypes(exchangeName) + if err != nil { + log.Printf("failed to get %s exchange asset types. Error: %s", + exchangeName, err) + continue + } + var tickerPrice ticker.Price + if len(assetTypes) > 1 { + for y := range assetTypes { + tickerPrice, err = individualBot.GetTickerPrice(currency, + assetTypes[y]) + } + } else { + tickerPrice, err = individualBot.GetTickerPrice(currency, + assetTypes[0]) + } + + if err != nil { + log.Printf("failed to get %s %s ticker. Error: %s", + currency.Pair().String(), + exchangeName, + err) + continue + } + + individualExchange.ExchangeValues = append( + individualExchange.ExchangeValues, tickerPrice, + ) + } + tickerData = append(tickerData, individualExchange) + } + } + return tickerData +} + +// RESTGetAllActiveTickers returns all active tickers +func RESTGetAllActiveTickers(w http.ResponseWriter, r *http.Request) { + var response AllEnabledExchangeCurrencies + response.Data = GetAllActiveTickers() + + err := RESTfulJSONResponse(w, r, response) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// GetAllEnabledExchangeAccountInfo returns all the current enabled exchanges +func GetAllEnabledExchangeAccountInfo() AllEnabledExchangeAccounts { + var response AllEnabledExchangeAccounts + for _, individualBot := range bot.exchanges { + if individualBot != nil && individualBot.IsEnabled() { + if !individualBot.GetAuthenticatedAPISupport() { + log.Printf("GetAllEnabledExchangeAccountInfo: Skippping %s due to disabled authenticated API support.", individualBot.GetName()) + continue + } + individualExchange, err := individualBot.GetExchangeAccountInfo() + if err != nil { + log.Printf("Error encountered retrieving exchange account info for %s. Error %s", + individualBot.GetName(), err) + continue + } + response.Data = append(response.Data, individualExchange) + } + } + return response +} + +// RESTGetAllEnabledAccountInfo via get request returns JSON response of account +// info +func RESTGetAllEnabledAccountInfo(w http.ResponseWriter, r *http.Request) { + response := GetAllEnabledExchangeAccountInfo() + err := RESTfulJSONResponse(w, r, response) + if err != nil { + RESTfulError(r.Method, err) + } +} diff --git a/routines.go b/routines.go new file mode 100644 index 00000000..5c32b6b0 --- /dev/null +++ b/routines.go @@ -0,0 +1,267 @@ +package main + +import ( + "fmt" + "log" + "time" + + "github.com/thrasher-/gocryptotrader/currency" + "github.com/thrasher-/gocryptotrader/currency/pair" + "github.com/thrasher-/gocryptotrader/currency/symbol" + exchange "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-/gocryptotrader/exchanges/stats" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" +) + +func printCurrencyFormat(price float64) string { + displaySymbol, err := symbol.GetSymbolByCurrencyName(bot.config.FiatDisplayCurrency) + if err != nil { + log.Printf("Failed to get display symbol: %s", err) + } + + return fmt.Sprintf("%s%.8f", displaySymbol, price) +} + +func printConvertCurrencyFormat(origCurrency string, origPrice float64) string { + displayCurrency := bot.config.FiatDisplayCurrency + conv, err := currency.ConvertCurrency(origPrice, origCurrency, displayCurrency) + if err != nil { + log.Printf("Failed to convert currency: %s", err) + } + + displaySymbol, err := symbol.GetSymbolByCurrencyName(displayCurrency) + if err != nil { + log.Printf("Failed to get display symbol: %s", err) + } + + origSymbol, err := symbol.GetSymbolByCurrencyName(origCurrency) + if err != nil { + log.Printf("Failed to get original currency symbol: %s", err) + } + + return fmt.Sprintf("%s%.2f %s (%s%.2f %s)", + displaySymbol, + conv, + displayCurrency, + origSymbol, + origPrice, + origCurrency, + ) +} + +func printSummary(result ticker.Price, p pair.CurrencyPair, assetType, exchangeName string, err error) { + if err != nil { + log.Printf("Failed to get %s %s ticker. Error: %s", + p.Pair().String(), + exchangeName, + err) + return + } + + stats.Add(exchangeName, p, assetType, result.Last, result.Volume) + if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.String() != bot.config.FiatDisplayCurrency { + origCurrency := p.SecondCurrency.Upper().String() + log.Printf("%s %s %s: Last %s Ask %s Bid %s High %s Low %s Volume %.8f", + exchangeName, + exchange.FormatCurrency(p).String(), + assetType, + printConvertCurrencyFormat(origCurrency, result.Last), + printConvertCurrencyFormat(origCurrency, result.Ask), + printConvertCurrencyFormat(origCurrency, result.Bid), + printConvertCurrencyFormat(origCurrency, result.High), + printConvertCurrencyFormat(origCurrency, result.Low), + result.Volume) + } else { + if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.Upper().String() == bot.config.FiatDisplayCurrency { + log.Printf("%s %s %s: Last %s Ask %s Bid %s High %s Low %s Volume %.8f", + exchangeName, + exchange.FormatCurrency(p).String(), + assetType, + printCurrencyFormat(result.Last), + printCurrencyFormat(result.Ask), + printCurrencyFormat(result.Bid), + printCurrencyFormat(result.High), + printCurrencyFormat(result.Low), + result.Volume) + } else { + log.Printf("%s %s %s: Last %.8f Ask %.8f Bid %.8f High %.8f Low %.8f Volume %.8f", + exchangeName, + exchange.FormatCurrency(p).String(), + assetType, + result.Last, + result.Ask, + result.Bid, + result.High, + result.Low, + result.Volume) + } + } +} + +func printOrderbookSummary(result orderbook.Base, p pair.CurrencyPair, assetType, exchangeName string, err error) { + if err != nil { + log.Printf("Failed to get %s %s orderbook. Error: %s", + p.Pair().String(), + exchangeName, + err) + return + } + bidsAmount, bidsValue := result.CalculateTotalBids() + asksAmount, asksValue := result.CalculateTotalAsks() + + if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.String() != bot.config.FiatDisplayCurrency { + origCurrency := p.SecondCurrency.Upper().String() + log.Printf("%s %s %s: Orderbook Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s", + exchangeName, + exchange.FormatCurrency(p).String(), + assetType, + len(result.Bids), + bidsAmount, + p.FirstCurrency.String(), + printConvertCurrencyFormat(origCurrency, bidsValue), + len(result.Asks), + asksAmount, + p.FirstCurrency.String(), + printConvertCurrencyFormat(origCurrency, asksValue), + ) + } else { + if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.Upper().String() == bot.config.FiatDisplayCurrency { + log.Printf("%s %s %s: Orderbook Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s", + exchangeName, + exchange.FormatCurrency(p).String(), + assetType, + len(result.Bids), + bidsAmount, + p.FirstCurrency.String(), + printCurrencyFormat(bidsValue), + len(result.Asks), + asksAmount, + p.FirstCurrency.String(), + printCurrencyFormat(asksValue), + ) + } else { + log.Printf("%s %s %s: Orderbook Bids len: %d Amount: %f %s. Total value: %f Asks len: %d Amount: %f %s. Total value: %f", + exchangeName, + exchange.FormatCurrency(p).String(), + assetType, + len(result.Bids), + bidsAmount, + p.FirstCurrency.String(), + bidsValue, + len(result.Asks), + asksAmount, + p.FirstCurrency.String(), + asksValue, + ) + } + } + +} + +func relayWebsocketEvent(result interface{}, event, assetType, exchangeName string) { + evt := WebsocketEvent{ + Data: result, + Event: event, + AssetType: assetType, + Exchange: exchangeName, + } + err := BroadcastWebsocketMessage(evt) + if err != nil { + log.Println(fmt.Errorf("Failed to broadcast websocket event. Error: %s", + err)) + } +} + +func TickerUpdaterRoutine() { + log.Println("Starting ticker updater routine") + for { + for x := range bot.exchanges { + if bot.exchanges[x].IsEnabled() { + exchangeName := bot.exchanges[x].GetName() + enabledCurrencies := bot.exchanges[x].GetEnabledCurrencies() + + var result ticker.Price + var err error + var assetTypes []string + + assetTypes, err = exchange.GetExchangeAssetTypes(exchangeName) + if err != nil { + log.Printf("failed to get %s exchange asset types. Error: %s", + exchangeName, err) + } + + for y := range enabledCurrencies { + currency := enabledCurrencies[y] + + if len(assetTypes) > 1 { + for z := range assetTypes { + result, err = bot.exchanges[x].UpdateTicker(currency, + assetTypes[z]) + printSummary(result, currency, assetTypes[z], exchangeName, err) + if err == nil { + relayWebsocketEvent(result, "ticker_update", assetTypes[z], exchangeName) + } + } + } else { + result, err = bot.exchanges[x].UpdateTicker(currency, + assetTypes[0]) + printSummary(result, currency, assetTypes[0], exchangeName, err) + if err == nil { + relayWebsocketEvent(result, "ticker_update", assetTypes[0], exchangeName) + } + } + } + } + } + time.Sleep(time.Second * 10) + } +} + +func OrderbookUpdaterRoutine() { + log.Println("Starting orderbook updater routine") + for { + for x := range bot.exchanges { + if bot.exchanges[x].IsEnabled() { + if bot.exchanges[x].GetName() == "ANX" { + continue + } + + exchangeName := bot.exchanges[x].GetName() + enabledCurrencies := bot.exchanges[x].GetEnabledCurrencies() + var result orderbook.Base + var err error + var assetTypes []string + + assetTypes, err = exchange.GetExchangeAssetTypes(exchangeName) + if err != nil { + log.Printf("failed to get %s exchange asset types. Error: %s", + exchangeName, err) + } + + for y := range enabledCurrencies { + currency := enabledCurrencies[y] + + if len(assetTypes) > 1 { + for z := range assetTypes { + result, err = bot.exchanges[x].UpdateOrderbook(currency, + assetTypes[z]) + printOrderbookSummary(result, currency, assetTypes[z], exchangeName, err) + if err == nil { + relayWebsocketEvent(result, "orderbook_update", assetTypes[z], exchangeName) + } + } + } else { + result, err = bot.exchanges[x].UpdateOrderbook(currency, + assetTypes[0]) + printOrderbookSummary(result, currency, assetTypes[0], exchangeName, err) + if err == nil { + relayWebsocketEvent(result, "orderbook_update", assetTypes[0], exchangeName) + } + } + } + } + } + time.Sleep(time.Second * 10) + } +} diff --git a/smsglobal/smsglobal.go b/smsglobal/smsglobal.go index 803407ab..d5cc5aab 100644 --- a/smsglobal/smsglobal.go +++ b/smsglobal/smsglobal.go @@ -2,62 +2,151 @@ package smsglobal import ( "errors" - "log" + "flag" "net/url" "strings" "github.com/thrasher-/gocryptotrader/common" - "github.com/thrasher-/gocryptotrader/config" ) const ( - smsGlobalAPIURL = "http://www.smsglobal.com/http-api.php" + smsGlobalAPIURL = "https://www.smsglobal.com/http-api.php" // ErrSMSContactNotFound is a general error code for "SMS Contact not found." ErrSMSContactNotFound = "SMS Contact not found." errSMSNotSent = "SMS message not sent." ) -// GetEnabledSMSContacts returns how many SMS contacts are enabled in the -// contacts list. -func GetEnabledSMSContacts(smsCfg config.SMSGlobalConfig) int { +// vars for the SMS global package +var ( + SMSGlobal *Base +) + +// Contact struct stores information related to a SMSGlobal contact +type Contact struct { + Name string `json:"name"` + Number string `json:"number"` + Enabled bool `json:"enabled"` +} + +// Base struct stores information related to the SMSGlobal package +type Base struct { + Contacts []Contact `json:"contacts"` + Username string `json:"username"` + Password string `json:"password"` + SendFrom string `json:"send_from"` +} + +// New initalises the SMSGlobal var +func New(username, password, sendFrom string, contacts []Contact) *Base { + if username == "" || password == "" || sendFrom == "" || len(contacts) == 0 { + return nil + } + + var goodContacts []Contact + for x := range contacts { + if contacts[x].Name != "" || contacts[x].Number != "" { + goodContacts = append(goodContacts, contacts[x]) + } + } + + SMSGlobal = &Base{ + Contacts: goodContacts, + Username: username, + Password: password, + SendFrom: sendFrom, + } + return SMSGlobal +} + +// GetEnabledContacts returns how many SMS contacts are enabled in the +// contact list +func (s *Base) GetEnabledContacts() int { counter := 0 - for _, contact := range smsCfg.Contacts { - if contact.Enabled { + for x := range s.Contacts { + if s.Contacts[x].Enabled { counter++ } } return counter } -// SMSSendToAll sends a message to all enabled contacts in cfg -func SMSSendToAll(message string, cfg config.Config) { - for _, contact := range cfg.SMS.Contacts { - if contact.Enabled && len(contact.Number) == 10 { - err := SMSNotify(contact.Number, message, cfg) - if err != nil { - log.Printf("Unable to send SMS to %s.\n", contact.Name) - } +// GetContactByNumber returns a contact with supplied number +func (s *Base) GetContactByNumber(number string) (Contact, error) { + for x := range s.Contacts { + if s.Contacts[x].Number == number { + return s.Contacts[x], nil + } + } + return Contact{}, errors.New(ErrSMSContactNotFound) +} + +// GetContactByName returns a contact with supplied name +func (s *Base) GetContactByName(name string) (Contact, error) { + for x := range s.Contacts { + if common.StringToLower(s.Contacts[x].Name) == common.StringToLower(name) { + return s.Contacts[x], nil + } + } + return Contact{}, errors.New(ErrSMSContactNotFound) +} + +// AddContact checks to see if a contact exists and adds them if it doesn't +func (s *Base) AddContact(contact Contact) { + if contact.Name == "" || contact.Number == "" { + return + } + + if s.ContactExists(contact) { + return + } + + s.Contacts = append(s.Contacts, contact) +} + +// ContactExists checks to see if a contact exists +func (s *Base) ContactExists(contact Contact) bool { + for x := range s.Contacts { + if s.Contacts[x].Number == contact.Number && common.StringToLower(s.Contacts[x].Name) == common.StringToLower(contact.Name) { + return true + } + } + return false +} + +// RemoveContact removes a contact if it exists +func (s *Base) RemoveContact(contact Contact) { + if !s.ContactExists(contact) { + return + } + + for x := range s.Contacts { + if s.Contacts[x].Name == contact.Name && s.Contacts[x].Number == contact.Number { + s.Contacts = append(s.Contacts[:x], s.Contacts[x+1:]...) + return } } } -// SMSGetNumberByName returns contact number by supplied name -func SMSGetNumberByName(name string, smsCfg config.SMSGlobalConfig) string { - for _, contact := range smsCfg.Contacts { - if contact.Name == name { - return contact.Number +// SendMessageToAll sends a message to all enabled contacts in cfg +func (s *Base) SendMessageToAll(message string) { + for x := range s.Contacts { + if s.Contacts[x].Enabled { + s.SendMessage(s.Contacts[x].Name, message) } } - return ErrSMSContactNotFound } -// SMSNotify sends a message to an individual contact -func SMSNotify(to, message string, cfg config.Config) error { +// SendMessage sends a message to an individual contact +func (s *Base) SendMessage(to, message string) error { + if flag.Lookup("test.v") != nil { + return nil + } + values := url.Values{} values.Set("action", "sendsms") - values.Set("user", cfg.SMS.Username) - values.Set("password", cfg.SMS.Password) - values.Set("from", cfg.Name) + values.Set("user", s.Username) + values.Set("password", s.Password) + values.Set("from", s.SendFrom) values.Set("to", to) values.Set("text", message) diff --git a/smsglobal/smsglobal_test.go b/smsglobal/smsglobal_test.go index 4cd57a17..8fcd287f 100644 --- a/smsglobal/smsglobal_test.go +++ b/smsglobal/smsglobal_test.go @@ -1,71 +1,138 @@ package smsglobal import ( + "log" "testing" - - "github.com/thrasher-/gocryptotrader/config" ) -func TestGetEnabledSMSContacts(t *testing.T) { - cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) - if err != nil { - t.Errorf( - "Test Failed. GetEnabledSMSContacts: Function return is incorrect with, %s.", - err, - ) +func TestNew(t *testing.T) { + result := New("", "", "", nil) + if result != nil { + t.Error("Test failed. New: Expected nil result") } - numberOfContacts := GetEnabledSMSContacts(cfg.SMS) - if numberOfContacts != len(cfg.SMS.Contacts) { - t.Errorf( - "Test Failed. GetEnabledSMSContacts: Function return is incorrect with, %d.", - numberOfContacts, - ) + + contact := Contact{Name: "bob", Number: "1234", Enabled: true} + var contacts []Contact + contacts = append(contacts, contact) + + result = New("bob", "pw", "Skynet", contacts) + if !result.ContactExists(contact) { + t.Error("Test failed. New: Expected contact not found") } } -func TestSMSSendToAll(t *testing.T) { - cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) - if err != nil { - t.Errorf( - "Test Failed. SMSSendToAll: \nFunction return is incorrect with, %s.", - err, - ) - } - SMSSendToAll("SMSGLOBAL Test - SMSSENDTOALL", *cfg) -} +func TestGetEnabledContacts(t *testing.T) { + contact := Contact{Name: "bob", Number: "1234", Enabled: true} + var contacts []Contact + contacts = append(contacts, contact) + result := New("bob", "pw", "Skynet", contacts) -func TestSMSGetNumberByName(t *testing.T) { - cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) - if err != nil { - t.Errorf( - "Test Failed. SMSGetNumberByName: Function return is incorrect with, %s.", - err, - ) - } - number := SMSGetNumberByName("StyleGherkin", cfg.SMS) - if number == "" { - t.Error("Test Failed. SMSNotify Error: No number, name not found.") - } - number = SMSGetNumberByName("testy", cfg.SMS) - if number == "" { - t.Error("Test Failed. SMSNotify Error: No number, name not found.") + expected := 1 + actual := result.GetEnabledContacts() + if expected != actual { + t.Errorf("Test failed. TestGetEnabledContacts expected %d, got %d", + expected, actual) } } -func TestSMSNotify(t *testing.T) { - cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) +func TestGetContactByNumber(t *testing.T) { + contact := Contact{Name: "bob", Number: "1234", Enabled: true} + var contacts []Contact + contacts = append(contacts, contact) + result := New("bob", "pw", "Skynet", contacts) + + actual, err := result.GetContactByNumber(contact.Number) if err != nil { - t.Errorf( - "Test Failed. SMSNotify: \nFunction return is incorrect with, %s.", - err, - ) + t.Fatalf("Test failed. TestGetContactByNumber: %s", err) + } + + if actual.Name != contact.Name && actual.Number != contact.Number && actual.Enabled != contact.Enabled { + t.Fatal("Test failed. TestGetContactByNumber: Incorrect values") + } + + _, err = result.GetContactByNumber("ASDASDASD") + if err == nil { + t.Fatal("Test failed. TestGetContactByNumber: Returned nil err on non-existant number") } - // err2 := SMSNotify("+61312112718", "teststring", *cfg) - // if err2 != nil { - // t.Error("Test Failed. SMSNotify: \nError: ", err2) - // } +} + +func TestGetContactByName(t *testing.T) { + contact := Contact{Name: "bob", Number: "1234", Enabled: true} + var contacts []Contact + contacts = append(contacts, contact) + result := New("bob", "pw", "Skynet", contacts) + + actual, err := result.GetContactByName(contact.Name) + if err != nil { + t.Fatalf("Test failed. TestGetContactByName: %s", err) + } + + if actual.Name != contact.Name && actual.Number != contact.Number && actual.Enabled != contact.Enabled { + t.Fatal("Test failed. TestGetContactByName: Incorrect values") + } + + _, err = result.GetContactByName("ASDASDASD") + if err == nil { + t.Fatal("Test failed. TestGetContactByName: Returned nil err on non-existant number") + } +} + +func TestAddContact(t *testing.T) { + contact := Contact{Name: "bob", Number: "1234", Enabled: true} + var contacts []Contact + contacts = append(contacts, contact) + result := New("bob", "pw", "Skynet", contacts) + + // Test adding same contact + result.AddContact(contact) + if result.GetEnabledContacts() > 1 { + t.Fatal("Test failed. TestAddContact: Incorrect values") + } + + invalidContact := Contact{Name: "", Number: "", Enabled: true} + result.AddContact(invalidContact) + if result.GetEnabledContacts() > 1 { + t.Fatal("Test failed. TestAddContact: Incorrect values") + } + + newContact := Contact{Name: "newContact", Number: "12345", Enabled: true} + result.AddContact(newContact) + if result.GetEnabledContacts() != 2 { + t.Fatal("Test failed. TestAddContact: Incorrect values") + } +} + +func TestRemoveContact(t *testing.T) { + contact := Contact{Name: "bob", Number: "1234", Enabled: true} + var contacts []Contact + contacts = append(contacts, contact) + result := New("bob", "pw", "Skynet", contacts) + + result.RemoveContact(Contact{Name: "blah", Number: "1234"}) + if result.GetEnabledContacts() != 1 { + t.Fatal("Test failed. TestRemoveContact: Incorrect values") + } + + result.RemoveContact(contact) + if result.GetEnabledContacts() != 0 { + t.Fatal("Test failed. TestRemoveContact: Incorrect values") + } +} + +func TestSendMessageToAll(t *testing.T) { + contact := Contact{Name: "bob", Number: "1234", Enabled: true} + var contacts []Contact + contacts = append(contacts, contact) + result := New("bob", "pw", "Skynet", contacts) + result.SendMessageToAll("hello world") +} + +func TestSendMessage(t *testing.T) { + contact := Contact{Name: "bob", Number: "1234", Enabled: true} + var contacts []Contact + contacts = append(contacts, contact) + result := New("bob", "pw", "Skynet", contacts) + err := result.SendMessage(contact.Number, "hello world") + log.Println(err) + t.Log(err) } diff --git a/testdata/configtest.dat b/testdata/configtest.dat index 9757e8c9..46541e00 100644 --- a/testdata/configtest.dat +++ b/testdata/configtest.dat @@ -2,35 +2,37 @@ "Name": "Skynet", "EncryptConfig": 0, "Cryptocurrencies": "BTC,LTC,ETH,XRP,NMC,NVC,PPC,XBT,DOGE,DASH", + "CurrencyExchangeProvider": "fixer", "CurrencyPairFormat": { "Uppercase": true, "Delimiter": "-" }, + "FiatDisplayCurrency": "USD", "PortfolioAddresses": { "Addresses": [ { "Address": "1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy", "CoinType": "BTC", - "Balance": 124178.0002442, - "Decscription": "" + "Balance": 124178.00647714, + "Description": "" }, { "Address": "3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v", "CoinType": "BTC", - "Balance": 103439.83659727, - "Decscription": "" + "Balance": 107843.84030984, + "Description": "" }, { "Address": "LgY8ahfHRhvjVQC1zJnBhFMG5pCTMuKRqh", "CoinType": "LTC", - "Balance": 3000000.05, - "Decscription": "" + "Balance": 100000.052, + "Description": "" }, { "Address": "0xb794f5ea0ba39494ce839613fffba74279579268", "CoinType": "ETH", - "Balance": 5774999.820458524, - "Decscription": "" + "Balance": 3.224999915984445e+24, + "Description": "" } ] }, @@ -50,7 +52,9 @@ "Enabled": false, "AdminUsername": "admin", "AdminPassword": "Password", - "ListenAddress": ":9050" + "ListenAddress": ":9050", + "WebsocketConnectionLimit": 1, + "WebsocketAllowInsecureOrigin": false }, "Exchanges": [ { @@ -64,7 +68,16 @@ "APISecret": "Secret", "AvailablePairs": "BTCUSD,BTCHKD,BTCEUR,BTCCAD,BTCAUD,BTCSGD,BTCJPY,BTCGBP,BTCNZD,LTCBTC,DOGEBTC,STRBTC,XRPBTC", "EnabledPairs": "BTCUSD,BTCHKD,BTCEUR,BTCCAD,BTCAUD,BTCSGD,BTCJPY,BTCGBP,BTCNZD,LTCBTC,DOGEBTC,STRBTC,XRPBTC", - "BaseCurrencies": "USD,HKD,EUR,CAD,AUD,SGD,JPY,GBP,NZD" + "BaseCurrencies": "USD,HKD,EUR,CAD,AUD,SGD,JPY,GBP,NZD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true, + "Index": "BTC" + }, + "RequestCurrencyPairFormat": { + "Uppercase": true, + "Index": "BTC" + } }, { "Name": "Bitfinex", @@ -75,9 +88,16 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "AvailablePairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC,ETCBTC,ETCUSD,BFXUSD,BFXBTC,RRTUSD,RRTBTC,ZECUSD,ZECBTC,XMRUSD,XMRBTC,DSHUSD,DSHBTC", + "AvailablePairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC,ETCBTC,ETCUSD,RRTUSD,RRTBTC,ZECUSD,ZECBTC,XMRUSD,XMRBTC,DSHUSD,DSHBTC,BCCBTC,BCUBTC,BCCUSD,BCUUSD,XRPUSD,XRPBTC,IOTUSD,IOTBTC,IOTETH,EOSUSD,EOSBTC,EOSETH,SANUSD,SANBTC,SANETH,OMGUSD,OMGBTC,OMGETH,BCHUSD,BCHBTC,BCHETH", "EnabledPairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC", - "BaseCurrencies": "USD" + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "Bitstamp", @@ -91,7 +111,36 @@ "ClientID": "ClientID", "AvailablePairs": "BTCUSD,BTCEUR,EURUSD,XRPUSD,XRPEUR", "EnabledPairs": "BTCUSD,BTCEUR,EURUSD,XRPUSD,XRPEUR", - "BaseCurrencies": "USD,EUR" + "BaseCurrencies": "USD,EUR", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } + }, + { + "Name": "Bittrex", + "Enabled": true, + "Verbose": false, + "Websocket": false, + "RESTPollingDelay": 10, + "AuthenticatedAPISupport": false, + "APIKey": "Key", + "APISecret": "Secret", + "AvailablePairs": "BTC-LTC,BTC-DOGE,BTC-VTC,BTC-PPC,BTC-FTC,BTC-RDD,BTC-NXT,BTC-DASH,BTC-POT,BTC-BLK,BTC-EMC2,BTC-XMY,BTC-AUR,BTC-EFL,BTC-GLD,BTC-SLR,BTC-PTC,BTC-GRS,BTC-NLG,BTC-RBY,BTC-XWC,BTC-MONA,BTC-THC,BTC-ENRG,BTC-ERC,BTC-VRC,BTC-CURE,BTC-XBB,BTC-XMR,BTC-CLOAK,BTC-START,BTC-KORE,BTC-XDN,BTC-TRUST,BTC-NAV,BTC-XST,BTC-BTCD,BTC-VIA,BTC-UNO,BTC-PINK,BTC-IOC,BTC-CANN,BTC-SYS,BTC-NEOS,BTC-DGB,BTC-BURST,BTC-EXCL,BTC-SWIFT,BTC-DOPE,BTC-BLOCK,BTC-ABY,BTC-BYC,BTC-XMG,BTC-BLITZ,BTC-BAY,BTC-BTS,BTC-FAIR,BTC-SPR,BTC-VTR,BTC-XRP,BTC-GAME,BTC-COVAL,BTC-NXS,BTC-XCP,BTC-BITB,BTC-GEO,BTC-FLDC,BTC-GRC,BTC-FLO,BTC-NBT,BTC-MUE,BTC-XEM,BTC-CLAM,BTC-DMD,BTC-GAM,BTC-SPHR,BTC-OK,BTC-SNRG,BTC-PKB,BTC-CPC,BTC-AEON,BTC-ETH,BTC-GCR,BTC-TX,BTC-BCY,BTC-EXP,BTC-INFX,BTC-OMNI,BTC-AMP,BTC-AGRS,BTC-XLM,BTC-BTA,USDT-BTC,BTC-CLUB,BTC-VOX,BTC-EMC,BTC-FCT,BTC-MAID,BTC-EGC,BTC-SLS,BTC-RADS,BTC-DCR,BTC-SAFEX,BTC-BSD,BTC-XVG,BTC-PIVX,BTC-XVC,BTC-MEME,BTC-STEEM,BTC-2GIVE,BTC-LSK,BTC-PDC,BTC-BRK,BTC-DGD,ETH-DGD,BTC-WAVES,BTC-RISE,BTC-LBC,BTC-SBD,BTC-BRX,BTC-DRACO,BTC-ETC,ETH-ETC,BTC-STRAT,BTC-UNB,BTC-SYNX,BTC-TRIG,BTC-EBST,BTC-VRM,BTC-SEQ,BTC-XAUR,BTC-SNGLS,BTC-REP,BTC-SHIFT,BTC-ARDR,BTC-XZC,BTC-NEO,BTC-ZEC,BTC-ZCL,BTC-IOP,BTC-DAR,BTC-GOLOS,BTC-HKG,BTC-UBQ,BTC-KMD,BTC-GBG,BTC-SIB,BTC-ION,BTC-LMC,BTC-QWARK,BTC-CRW,BTC-SWT,BTC-TIME,BTC-MLN,BTC-ARK,BTC-DYN,BTC-TKS,BTC-MUSIC,BTC-DTB,BTC-INCNT,BTC-GBYTE,BTC-GNT,BTC-NXC,BTC-EDG,BTC-LGD,BTC-TRST,ETH-GNT,ETH-REP,USDT-ETH,ETH-WINGS,BTC-WINGS,BTC-RLC,BTC-GNO,BTC-GUP,BTC-LUN,ETH-GUP,ETH-RLC,ETH-LUN,ETH-SNGLS,ETH-GNO,BTC-APX,BTC-TKN,ETH-TKN,BTC-HMQ,ETH-HMQ,BTC-ANT,ETH-TRST,ETH-ANT,BTC-SC,ETH-BAT,BTC-BAT,BTC-ZEN,BTC-1ST,BTC-QRL,ETH-1ST,ETH-QRL,BTC-CRB,ETH-CRB,ETH-LGD,BTC-PTOY,ETH-PTOY,BTC-MYST,ETH-MYST,BTC-CFI,ETH-CFI,BTC-BNT,ETH-BNT,BTC-NMR,ETH-NMR,ETH-TIME,ETH-LTC,ETH-XRP,BTC-SNT,ETH-SNT,BTC-DCT,BTC-XEL,BTC-MCO,ETH-MCO,BTC-ADT,ETH-ADT,BTC-FUN,ETH-FUN,BTC-PAY,ETH-PAY,BTC-MTL,ETH-MTL,BTC-STORJ,ETH-STORJ,BTC-ADX,ETH-ADX,ETH-DASH,ETH-SC,ETH-ZEC,USDT-ZEC,USDT-LTC,USDT-ETC,USDT-XRP,BTC-OMG,ETH-OMG,BTC-CVC,ETH-CVC,BTC-PART,BTC-QTUM,ETH-QTUM,ETH-XMR,ETH-XEM,ETH-XLM,ETH-NEO,USDT-XMR,USDT-DASH,ETH-BCC,USDT-BCC,BTC-BCC,USDT-NEO,ETH-WAVES,ETH-STRAT,ETH-DGB,ETH-FCT,ETH-BTS", + "EnabledPairs": "USDT-BTC", + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "-" + }, + "RequestCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "-" + } }, { "Name": "BTCC", @@ -104,7 +153,14 @@ "APISecret": "Secret", "AvailablePairs": "BTCCNY,LTCCNY,LTCBTC", "EnabledPairs": "BTCCNY,LTCCNY,LTCBTC", - "BaseCurrencies": "CNY" + "BaseCurrencies": "CNY", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": false + } }, { "Name": "BTCE", @@ -117,7 +173,16 @@ "APISecret": "Secret", "AvailablePairs": "BTCUSD,BTCRUR,BTCEUR,LTCBTC,LTCUSD,LTCRUR,LTCEUR,NMCBTC,NMCUSD,NVCBTC,NVCUSD,USDRUR,EURUSD,EURRUR,PPCBTC,PPCUSD", "EnabledPairs": "BTCUSD,BTCRUR,BTCEUR,LTCBTC,LTCUSD,LTCRUR,LTCEUR,NMCBTC,NMCUSD,NVCBTC,NVCUSD,USDRUR,EURUSD,EURRUR,PPCBTC,PPCUSD", - "BaseCurrencies": "USD,RUR,EUR" + "BaseCurrencies": "USD,RUR,EUR", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": false, + "Delimiter": "_", + "Separator": "-" + } }, { "Name": "BTC Markets", @@ -128,9 +193,16 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "AvailablePairs": "LTC,BTC", - "EnabledPairs": "LTC,BTC", - "BaseCurrencies": "AUD" + "AvailablePairs": "LTCAUD,BTCAUD", + "EnabledPairs": "LTCAUD,BTCAUD", + "BaseCurrencies": "AUD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "GDAX", @@ -142,9 +214,17 @@ "APIKey": "Key", "APISecret": "Secret", "ClientID": "ClientID", - "AvailablePairs": "BTCGBP,BTCEUR,ETHUSD,ETHBTC,LTCUSD,LTCBTC,BTCUSD", + "AvailablePairs": "LTCEUR,LTCBTC,BTCGBP,BTCEUR,ETHEUR,ETHBTC,LTCUSD,BTCUSD,ETHUSD", "EnabledPairs": "BTCUSD,BTCGBP,BTCEUR", - "BaseCurrencies": "USD,GBP,EUR" + "BaseCurrencies": "USD,GBP,EUR", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "-" + } }, { "Name": "Gemini", @@ -157,7 +237,14 @@ "APISecret": "Secret", "AvailablePairs": "BTCUSD,ETHBTC,ETHUSD", "EnabledPairs": "BTCUSD", - "BaseCurrencies": "USD" + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "Huobi", @@ -170,7 +257,14 @@ "APISecret": "Secret", "AvailablePairs": "BTCCNY,LTCCNY", "EnabledPairs": "BTCCNY,LTCCNY", - "BaseCurrencies": "CNY" + "BaseCurrencies": "CNY", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": false + } }, { "Name": "ITBIT", @@ -184,7 +278,14 @@ "ClientID": "ClientID", "AvailablePairs": "XBTUSD,XBTSGD,XBTEUR", "EnabledPairs": "XBTUSD,XBTSGD,XBTEUR", - "BaseCurrencies": "USD,SGD,EUR" + "BaseCurrencies": "USD,SGD,EUR", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "Kraken", @@ -195,9 +296,17 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "AvailablePairs": "ETCUSD,ICNETH,REPXBT,ZECXBT,ETHXBT,ETHXBT.d,ETHGBP,LTCXBT,XBTGBP.d,XDGXBT,XMRUSD,ZECUSD,ETCETH,ETHJPY,XBTCAD.d,XBTJPY.d,XBTUSD.d,XLMXBT,XLMEUR,XLMUSD,XMREUR,ETCXBT,ETHCAD.d,ETHEUR.d,ETHJPY.d,XBTEUR.d,ETHEUR,ETHGBP.d,ICNXBT,LTCEUR,REPEUR,XBTGBP,XBTJPY,ETHUSD,ETHUSD.d,LTCUSD,REPETH,XBTUSD,XMRXBT,ETCEUR,ETHCAD,REPUSD,XBTCAD,XBTEUR,XRPXBT,ZECEUR", + "AvailablePairs": "ETHEUR,XRPXBT,BCHXBT,DASHUSD,EOSETH,REPXBT,XBTUSD.D,XLMXBT,ETHGBP.D,XMRXBT,GNOXBT,ETHUSD,ETCXBT,ETHEUR.D,ICNXBT,XBTJPY.D,XRPUSD,BCHEUR,DASHXBT,ETHCAD,ZECUSD,ICNETH,MLNETH,XDGXBT,GNOETH,LTCUSD,XBTCAD,XBTEUR,ZECXBT,BCHUSD,DASHEUR,EOSXBT,USDTUSD,ETCUSD,ETHXBT,ETHXBT.D,XBTJPY,XBTCAD.D,XRPEUR,LTCXBT,REPETH,XBTGBP.D,REPEUR,XMRUSD,ETHCAD.D,ETHJPY,ETHJPY.D,ETCETH,XBTEUR.D,XBTGBP,LTCEUR,MLNXBT,XBTUSD,XMREUR,ZECEUR,ETCEUR,ETHGBP,ETHUSD.D", "EnabledPairs": "ETCUSD,XBTUSD,ETHUSD", - "BaseCurrencies": "EUR,USD,CAD,GBP,JPY" + "BaseCurrencies": "EUR,USD,CAD,GBP,JPY", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true, + "Separator": "," + } }, { "Name": "LakeBTC", @@ -210,7 +319,14 @@ "APISecret": "Secret", "AvailablePairs": "BTCUSD,BTCEUR,USDHKD,AUDUSD,BTCGBP,BTCNZD,USDJPY,BTCSGD,BTCNGN,EURUSD,USDSGD,NZDUSD,USDNGN,USDCHF,BTCJPY,BTCAUD,BTCCAD,BTCCHF,GBPUSD,USDCAD", "EnabledPairs": "BTCUSD,BTCAUD", - "BaseCurrencies": "USD,EUR,HKD,AUD,GBP,NZD,JPY,SGD,NGN,CHF,CAD" + "BaseCurrencies": "USD,EUR,HKD,AUD,GBP,NZD,JPY,SGD,NGN,CHF,CAD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "Liqui", @@ -221,9 +337,19 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "AvailablePairs": "TIME_BTC,ETH_BTC,GNT_BTC,WAVES_BTC,ICN_BTC,1ST_BTC,WINGS_BTC,MLN_BTC,ROUND_BTC,VSL_BTC,LTC_BTC,DCT_BTC,INCNT_BTC,PLU_BTC,DASH_BTC", + "AvailablePairs": "HMQ_ETH,PTOY_ETH,SNT_BTC,TRST_ETH,RLC_BTC,TRST_BTC,LUN_ETH,XID_USDT,DASH_BTC,ICN_USDT,BNT_ETH,TIME_ETH,VSL_BTC,PLU_ETH,1ST_USDT,RLC_ETH,GNO_ETH,TKN_BTC,BCC_ETH,GNT_BTC,ROUND_ETH,EDG_BTC,PAY_USDT,INCNT_USDT,DGD_USDT,LTC_BTC,DASH_USDT,MCO_USDT,OMG_ETH,CVC_BTC,BCC_BTC,DNT_BTC,INCNT_ETH,GUP_BTC,TAAS_ETH,QRL_BTC,ZRX_USDT,1ST_ETH,MYST_ETH,TNT_USDT,STORJ_ETH,NET_USDT,OAX_ETH,OAX_USDT,ZRX_BTC,GNO_BTC,CFI_BTC,NET_ETH,TAAS_USDT,WINGS_ETH,HMQ_BTC,BAT_BTC,PTOY_BTC,PAY_BTC,1ST_BTC,ROUND_USDT,SNGLS_ETH,SNM_ETH,NET_BTC,BTC_USDT,TKN_ETH,HMQ_USDT,MGO_USDT,WINGS_USDT,MGO_BTC,ADX_USDT,DASH_ETH,VSL_ETH,GNT_USDT,MLN_USDT,RLC_USDT,TKN_USDT,ZRX_ETH,ROUND_BTC,QTUM_USDT,STORJ_BTC,MCO_BTC,MCO_ETH,ADX_BTC,EOS_USDT,XID_ETH,STX_USDT,ETH_BTC,MLN_ETH,EDG_USDT,PLU_USDT,LUN_USDT,ANT_USDT,SAN_ETH,TIME_BTC,WAVES_ETH,REP_USDT,BCAP_BTC,SNM_USDT,SNT_USDT,TNT_BTC,WAVES_BTC,GUP_ETH,BCAP_ETH,BNT_BTC,BNT_USDT,SNT_ETH,XID_BTC,DGD_BTC,ICN_ETH,DGD_ETH,LTC_USDT,TIME_USDT,REP_ETH,ANT_ETH,BAT_ETH,ADX_ETH,SAN_BTC,ICN_BTC,QTUM_BTC,MGO_ETH,MYST_USDT,EOS_BTC,OMG_USDT,OAX_BTC,MLN_BTC,TAAS_BTC,WINGS_BTC,SNGLS_BTC,CFI_USDT,SNM_BTC,EOS_ETH,STX_ETH,QRL_ETH,CFI_ETH,STORJ_USDT,SAN_USDT,DNT_USDT,LTC_ETH,VSL_USDT,WAVES_USDT,TRST_USDT,PTOY_USDT,ETH_USDT,GUP_USDT,PAY_ETH,OMG_BTC,INCNT_BTC,CVC_ETH,GNT_ETH,REP_BTC,GNO_USDT,LUN_BTC,MYST_BTC,SNGLS_USDT,QTUM_ETH,PLU_BTC,BCC_USDT,BCAP_USDT,ANT_BTC,BAT_USDT,QRL_USDT,CVC_USDT,DNT_ETH,STX_BTC,EDG_ETH,TNT_ETH", "EnabledPairs": "ETH_BTC,LTC_BTC,DASH_BTC", - "BaseCurrencies": "USD" + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "_" + }, + "RequestCurrencyPairFormat": { + "Uppercase": false, + "Delimiter": "_", + "Separator": "-" + } }, { "Name": "LocalBitcoins", @@ -236,7 +362,14 @@ "APISecret": "Secret", "AvailablePairs": "BTCARS,BTCAUD,BTCBRL,BTCCAD,BTCCHF,BTCCZK,BTCDKK,BTCEUR,BTCGBP,BTCHKD,BTCILS,BTCINR,BTCMXN,BTCNOK,BTCNZD,BTCPLN,BTCRUB,BTCSEK,BTCSGD,BTCTHB,BTCUSD,BTCZAR", "EnabledPairs": "BTCARS,BTCAUD,BTCBRL,BTCCAD,BTCCHF,BTCCZK,BTCDKK,BTCEUR,BTCGBP,BTCHKD,BTCILS,BTCINR,BTCMXN,BTCNOK,BTCNZD,BTCPLN,BTCRUB,BTCSEK,BTCSGD,BTCTHB,BTCUSD,BTCZAR", - "BaseCurrencies": "ARS,AUD,BRL,CAD,CHF,CZK,DKK,EUR,GBP,HKD,ILS,INR,MXN,NOK,NZD,PLN,RUB,SEK,SGD,THB,USD,ZAR" + "BaseCurrencies": "ARS,AUD,BRL,CAD,CHF,CZK,DKK,EUR,GBP,HKD,ILS,INR,MXN,NOK,NZD,PLN,RUB,SEK,SGD,THB,USD,ZAR", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } }, { "Name": "OKCOIN China", @@ -249,7 +382,15 @@ "APISecret": "Secret", "AvailablePairs": "BTCCNY,LTCCNY", "EnabledPairs": "BTCCNY,LTCCNY", - "BaseCurrencies": "CNY" + "BaseCurrencies": "CNY", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": false, + "Delimiter": "_" + } }, { "Name": "OKCOIN International", @@ -262,7 +403,15 @@ "APISecret": "Secret", "AvailablePairs": "BTCUSD,LTCUSD", "EnabledPairs": "BTCUSD,LTCUSD", - "BaseCurrencies": "USD" + "BaseCurrencies": "USD", + "AssetTypes": "SPOT,this_week,next_week,quarter", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": false, + "Delimiter": "_" + } }, { "Name": "Poloniex", @@ -275,7 +424,16 @@ "APISecret": "Secret", "AvailablePairs": "BTC_XUSD,BTC_FCT,BTC_MMNXT,BTC_NMC,BTC_BITUSD,BTC_RDD,BTC_XMR,BTC_XST,BTC_DSH,BTC_MAID,BTC_DGB,BTC_NEOS,BTC_BLK,BTC_NAUT,BTC_NBT,BTC_XCP,BTC_STR,BTC_BTCD,BTC_GRC,BTC_HUC,BTC_BBR,BTC_XDN,BTC_INDEX,BTC_IOC,BTC_SWARM,BTC_EMC2,BTC_MCN,BTC_NOXT,BTC_MINT,BTC_PTS,BTC_SC,BTC_GEO,BTC_XRP,BTC_FLO,BTC_BITS,BTC_HYP,BTC_XCR,BTC_LTBC,BTC_SYS,BTC_GMC,BTC_ETH,BTC_SYNC,BTC_GAP,BTC_BCN,BTC_C2,BTC_PINK,BTC_FIBRE,BTC_POT,BTC_QTL,BTC_SDC,BTC_XC,BTC_DASH,BTC_SILK,BTC_CLAM,BTC_NAV,BTC_PIGGY,BTC_BCY,BTC_MIL,BTC_XCN,BTC_YACC,BTC_BTS,BTC_QBK,BTC_SJCX,BTC_LQD,BTC_BURST,BTC_RIC,BTC_VRC,BTC_LTC,BTC_XPB,BTC_GRS,BTC_XCH,BTC_ARCH,BTC_QORA,BTC_HZ,BTC_NSR,BTC_XPM,BTC_BITCNY,BTC_EXE,BTC_XMG,BTC_BTC,BTC_BTM,BTC_NOBL,BTC_NXT,BTC_DOGE,BTC_CURE,BTC_MNTA,BTC_ADN,BTC_EXP,BTC_VTC,BTC_FLDC,BTC_MRS,BTC_MYR,BTC_OMNI,BTC_VNL,BTC_USDT,BTC_NOTE,BTC_WDC,BTC_BELA,BTC_VIA,BTC_CGA,BTC_DIEM,BTC_IFC,BTC_XDP,BTC_BLOCK,BTC_MMC,BTC_1CR,BTC_UNITY,BTC_XBC,BTC_GEMZ,BTC_FLT,BTC_PPC,BTC_XEM,BTC_RBY,BTC_CNMT,BTC_ABY,XMR_XDN,XMR_IFC,XMR_DIEM,XMR_BBR,XMR_DSH,XMR_BCN,XMR_LTC,XMR_MAID,XMR_DASH,XMR_BTCD,XMR_HYP,XMR_BLK,XMR_QORA,XMR_MNTA,XMR_NXT,USDT_BTC,USDT_ETH,USDT_XRP,USDT_DASH,USDT_LTC,USDT_NXT,USDT_XMR,USDT_STR", "EnabledPairs": "BTC_LTC,BTC_ETH,BTC_DOGE,BTC_DASH,BTC_XRP", - "BaseCurrencies": "USD" + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "_" + }, + "RequestCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "_" + } } ] } \ No newline at end of file diff --git a/ticker_routes.go b/ticker_routes.go deleted file mode 100644 index 4ec16ea3..00000000 --- a/ticker_routes.go +++ /dev/null @@ -1,103 +0,0 @@ -package main - -import ( - "encoding/json" - "log" - "net/http" - - "github.com/gorilla/mux" - "github.com/thrasher-/gocryptotrader/currency/pair" - "github.com/thrasher-/gocryptotrader/exchanges/ticker" -) - -func jsonTickerResponse(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - currency := vars["currency"] - exchangeName := vars["exchangeName"] - var response ticker.TickerPrice - var err error - for i := 0; i < len(bot.exchanges); i++ { - if bot.exchanges[i] != nil { - if bot.exchanges[i].IsEnabled() && bot.exchanges[i].GetName() == exchangeName { - response, err = bot.exchanges[i].GetTickerPrice( - pair.NewCurrencyPairFromString(currency), - ) - if err != nil { - log.Println(err) - continue - } - } - } - } - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - encoder := json.NewEncoder(w) - - if err = encoder.Encode(response); err != nil { - panic(err) - } -} - -// AllEnabledExchangeCurrencies holds the enabled exchange currencies -type AllEnabledExchangeCurrencies struct { - Data []EnabledExchangeCurrencies `json:"data"` -} - -// EnabledExchangeCurrencies is a sub type for singular exchanges and respective -// currencies -type EnabledExchangeCurrencies struct { - ExchangeName string `json:"exchangeName"` - ExchangeValues []ticker.TickerPrice `json:"exchangeValues"` -} - -func getAllActiveTickersResponse(w http.ResponseWriter, r *http.Request) { - var response AllEnabledExchangeCurrencies - - for _, individualBot := range bot.exchanges { - if individualBot != nil && individualBot.IsEnabled() { - var individualExchange EnabledExchangeCurrencies - individualExchange.ExchangeName = individualBot.GetName() - log.Println( - "Getting enabled currencies for '" + individualBot.GetName() + "'", - ) - currencies := individualBot.GetEnabledCurrencies() - log.Println(currencies) - for _, currency := range currencies { - tickerPrice, err := individualBot.GetTickerPrice( - pair.NewCurrencyPairFromString(currency), - ) - if err != nil { - continue - } - log.Println(tickerPrice) - - individualExchange.ExchangeValues = append( - individualExchange.ExchangeValues, tickerPrice, - ) - } - response.Data = append(response.Data, individualExchange) - } - } - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(response); err != nil { - panic(err) - } -} - -// ExchangeRoutes denotes the current exchange routes -var ExchangeRoutes = Routes{ - Route{ - "AllActiveExchangesAndCurrencies", - "GET", - "/exchanges/enabled/latest/all", - getAllActiveTickersResponse, - }, - Route{ - "IndividualExchangeAndCurrency", - "GET", - "/exchanges/{exchangeName}/latest/{currency}", - jsonTickerResponse, - }, -} diff --git a/ticker_routes_test.go b/ticker_routes_test.go deleted file mode 100644 index 06ab7d0f..00000000 --- a/ticker_routes_test.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/tools/portfolio/portfolio.go b/tools/portfolio/portfolio.go index a7b806fb..bc47dbd1 100644 --- a/tools/portfolio/portfolio.go +++ b/tools/portfolio/portfolio.go @@ -9,22 +9,33 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/currency" + "github.com/thrasher-/gocryptotrader/currency/symbol" "github.com/thrasher-/gocryptotrader/exchanges/bitfinex" "github.com/thrasher-/gocryptotrader/portfolio" ) var ( - priceMap map[string]float64 + priceMap map[string]float64 + displayCurrency string ) -func printSummary(msg, from, to string, amount float64) { +func printSummary(msg string, amount float64) { log.Println() log.Println(fmt.Sprintf("%s in USD: $%.2f", msg, amount)) - conv, err := currency.ConvertCurrency(amount, "USD", "AUD") - if err != nil { - log.Println(err) - } else { - log.Println(fmt.Sprintf("%s in AUD: $%.2f", msg, conv)) + + if displayCurrency != "USD" { + conv, err := currency.ConvertCurrency(amount, "USD", displayCurrency) + if err != nil { + log.Println(err) + } else { + symb, err := symbol.GetSymbolByCurrencyName(displayCurrency) + if err != nil { + log.Println(fmt.Sprintf("%s in %s: %.2f", msg, displayCurrency, conv)) + } else { + log.Println(fmt.Sprintf("%s in %s: %s%.2f", msg, displayCurrency, symb, conv)) + } + + } } log.Println() } @@ -38,9 +49,9 @@ func getOnlineOfflinePortfolio(coins []portfolio.Coin, online bool) { x.Balance, value, x.Percentage) } if !online { - printSummary("\tOffline balance", "USD", "AUD", totals) + printSummary("\tOffline balance", totals) } else { - printSummary("\tOnline balance", "USD", "AUD", totals) + printSummary("\tOnline balance", totals) } } @@ -53,12 +64,13 @@ func main() { log.Println("GoCryptoTrader: portfolio tool.") var cfg config.Config - var err = cfg.ReadConfig(inFile) + var err = cfg.LoadConfig(inFile) if err != nil { log.Fatal(err) } log.Println("Loaded config file.") + displayCurrency = cfg.FiatDisplayCurrency port := portfolio.Base{} port.SeedPortfolio(cfg.Portfolio) result := port.GetPortfolioSummary() @@ -127,7 +139,7 @@ func main() { for x, y := range portfolioMap { log.Printf("\t%s Amount: %f Subtotal: $%.2f USD (1 %s = $%.2f USD). Percentage of portfolio %.3f%%", x, y.Balance, y.Subtotal, x, y.Subtotal/y.Balance, y.Subtotal/total*100/1) } - printSummary("\tTotal balance", "USD", "AUD", total) + printSummary("\tTotal balance", total) log.Println("OFFLINE COIN TOTALS:") getOnlineOfflinePortfolio(result.Offline, false) @@ -146,7 +158,7 @@ func main() { log.Printf("\t %s Amount: %f Subtotal: $%.2f Coin percentage: %.2f%%\n", y[z].Address, y[z].Balance, value, y[z].Percentage) } - printSummary(fmt.Sprintf("\t %s balance", x), "USD", "AUD", totals) + printSummary(fmt.Sprintf("\t %s balance", x), totals) } log.Println("ONLINE COINS SUMMARY:") @@ -159,6 +171,6 @@ func main() { log.Printf("\t %s Amount: %f Subtotal $%.2f Coin percentage: %.2f%%", z, w.Balance, value, w.Percentage) } - printSummary("\t Exchange balance", "USD", "AUD", totals) + printSummary("\t Exchange balance", totals) } } diff --git a/tools/websocket_client/main.go b/tools/websocket_client/main.go new file mode 100644 index 00000000..4cb31fe1 --- /dev/null +++ b/tools/websocket_client/main.go @@ -0,0 +1,191 @@ +package main + +import ( + "errors" + "fmt" + "log" + "net/http" + + "github.com/gorilla/websocket" + "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/config" +) + +// Vars for the websocket client +var ( + WSConn *websocket.Conn +) + +// WebsocketEvent is the struct used for websocket events +type WebsocketEvent struct { + Exchange string `json:"exchange,omitempty"` + AssetType string `json:"assetType,omitempty"` + Event string + Data interface{} +} + +// WebsocketAuth is the struct used for a websocket auth request +type WebsocketAuth struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// WebsocketEventResponse is the struct used for websocket event responses +type WebsocketEventResponse struct { + Event string `json:"event"` + Data interface{} `json:"data"` + Error string `json:"error"` +} + +// WebsocketOrderbookTickerRequest is a struct used for ticker and orderbook +// requests +type WebsocketOrderbookTickerRequest struct { + Exchange string `json:"exchangeName"` + Currency string `json:"currency"` + AssetType string `json:"assetType"` +} + +// SendWebsocketEvent sends a websocket event message +func SendWebsocketEvent(event string, reqData interface{}, result *WebsocketEventResponse) error { + req := WebsocketEvent{ + Event: event, + } + + if reqData != nil { + req.Data = reqData + } + + err := WSConn.WriteJSON(req) + if err != nil { + return err + } + + err = WSConn.ReadJSON(&result) + if err != nil { + return err + } + + if result.Error != "" { + return errors.New(result.Error) + } + + return nil +} + +func main() { + cfg := config.GetConfig() + err := cfg.LoadConfig(config.ConfigFile) + if err != nil { + log.Fatalf("Failed to load config file: %s", err) + } + + listenAddr := cfg.Webserver.ListenAddress + wsHost := fmt.Sprintf("ws://%s:%d/ws", common.ExtractHost(listenAddr), + common.ExtractPort(listenAddr)) + log.Printf("Connecting to websocket host: %s", wsHost) + + var Dialer websocket.Dialer + WSConn, _, err = Dialer.Dial(wsHost, http.Header{}) + if err != nil { + log.Println("Unable to connect to websocket server") + return + } + log.Println("Connected to websocket!") + + log.Println("Authenticating..") + var wsResp WebsocketEventResponse + reqData := WebsocketAuth{ + Username: cfg.Webserver.AdminUsername, + Password: common.HexEncodeToString(common.GetSHA256([]byte(cfg.Webserver.AdminPassword))), + } + err = SendWebsocketEvent("auth", reqData, &wsResp) + if err != nil { + log.Fatal(err) + } + log.Println("Authenticated successfully") + + log.Println("Getting config..") + err = SendWebsocketEvent("GetConfig", nil, &wsResp) + if err != nil { + log.Fatal(err) + } + log.Printf("Fetched config.") + + dataJSON, err := common.JSONEncode(&wsResp.Data) + if err != nil { + log.Fatal(err) + } + + var resultCfg config.Config + err = common.JSONDecode(dataJSON, &resultCfg) + if err != nil { + log.Fatal(err) + } + + log.Println("Saving config..") + origBotName := resultCfg.Name + resultCfg.Name = "TEST" + err = SendWebsocketEvent("SaveConfig", resultCfg, &wsResp) + if err != nil { + log.Fatal(err) + } + log.Println("Saved config!") + resultCfg.Name = origBotName + err = SendWebsocketEvent("SaveConfig", resultCfg, &wsResp) + if err != nil { + log.Fatal(err) + } + log.Println("Saved config (restored original bot name)!") + + log.Println("Getting account info..") + err = SendWebsocketEvent("GetAccountInfo", nil, &wsResp) + if err != nil { + log.Fatal(err) + } + log.Println("Got account info!") + + log.Println("Getting tickers..") + err = SendWebsocketEvent("GetTickers", nil, &wsResp) + if err != nil { + log.Fatal(err) + } + log.Println("Got tickers!") + + log.Println("Getting specific ticker..") + dataReq := WebsocketOrderbookTickerRequest{ + Exchange: "Bitfinex", + Currency: "BTCUSD", + AssetType: "SPOT", + } + + err = SendWebsocketEvent("GetTicker", dataReq, &wsResp) + if err != nil { + log.Fatal(err) + } + log.Println("Got ticker!") + + log.Println("Getting orderbooks..") + err = SendWebsocketEvent("GetOrderbooks", nil, &wsResp) + if err != nil { + log.Fatal(err) + } + log.Println("Got orderbooks!") + + log.Println("Getting specific orderbook..") + err = SendWebsocketEvent("GetOrderbook", dataReq, &wsResp) + if err != nil { + log.Fatal(err) + } + log.Println("Got orderbook!") + + for { + var wsEvent WebsocketEventResponse + err = WSConn.ReadJSON(&wsEvent) + if err != nil { + break + } + + log.Printf("Recv'd: %s", wsEvent.Event) + } + WSConn.Close() +} diff --git a/wallet_routes.go b/wallet_routes.go deleted file mode 100644 index 3ed2e1f3..00000000 --- a/wallet_routes.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "log" - "net/http" - - "github.com/thrasher-/gocryptotrader/exchanges" -) - -// AllEnabledExchangeAccounts holds all enabled accounts info -type AllEnabledExchangeAccounts struct { - Data []exchange.AccountInfo `json:"data"` -} - -// GetCollatedExchangeAccountInfoByCoin collates individual exchange account -// information and turns into into a map string of -// exchange.AccountCurrencyInfo -func GetCollatedExchangeAccountInfoByCoin(accounts []exchange.AccountInfo) map[string]exchange.AccountCurrencyInfo { - result := make(map[string]exchange.AccountCurrencyInfo) - for i := 0; i < len(accounts); i++ { - for j := 0; j < len(accounts[i].Currencies); j++ { - currencyName := accounts[i].Currencies[j].CurrencyName - avail := accounts[i].Currencies[j].TotalValue - onHold := accounts[i].Currencies[j].Hold - - info, ok := result[currencyName] - if !ok { - accountInfo := exchange.AccountCurrencyInfo{CurrencyName: currencyName, Hold: onHold, TotalValue: avail} - result[currencyName] = accountInfo - } else { - info.Hold += onHold - info.TotalValue += avail - result[currencyName] = info - } - } - } - return result -} - -// GetAccountCurrencyInfoByExchangeName returns info for an exchange -func GetAccountCurrencyInfoByExchangeName(accounts []exchange.AccountInfo, exchangeName string) (exchange.AccountInfo, error) { - for i := 0; i < len(accounts); i++ { - if accounts[i].ExchangeName == exchangeName { - return accounts[i], nil - } - } - return exchange.AccountInfo{}, errors.New(exchange.ErrExchangeNotFound) -} - -// GetAllEnabledExchangeAccountInfo returns all the current enabled exchanges -func GetAllEnabledExchangeAccountInfo() AllEnabledExchangeAccounts { - var response AllEnabledExchangeAccounts - for _, individualBot := range bot.exchanges { - if individualBot != nil && individualBot.IsEnabled() { - if !individualBot.GetAuthenticatedAPISupport() { - log.Printf("GetAllEnabledExchangeAccountInfo: Skippping %s due to disabled authenticated API support.", individualBot.GetName()) - continue - } - individualExchange, err := individualBot.GetExchangeAccountInfo() - if err != nil { - log.Printf("Error encountered retrieving exchange account info for %s. Error %s", - individualBot.GetName(), err) - continue - } - response.Data = append(response.Data, individualExchange) - } - } - return response -} - -// SendAllEnabledAccountInfo via get request returns JSON response of account -// info -func SendAllEnabledAccountInfo(w http.ResponseWriter, r *http.Request) { - response := GetAllEnabledExchangeAccountInfo() - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(response); err != nil { - panic(err) - } -} - -// WalletRoutes are current routes specified for queries. -var WalletRoutes = Routes{ - Route{ - "AllEnabledAccountInfo", - "GET", - "/exchanges/enabled/accounts/all", - SendAllEnabledAccountInfo, - }, -} diff --git a/wallet_routes_test.go b/wallet_routes_test.go deleted file mode 100644 index b8f5d9b9..00000000 --- a/wallet_routes_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "testing" -) - -func TestGetCollatedExchangeAccountInfoByCoin(t *testing.T) { - GetCollatedExchangeAccountInfoByCoin(GetAllEnabledExchangeAccountInfo().Data) -} - -func TestGetAccountCurrencyInfoByExchangeName(t *testing.T) { - _, err := GetAccountCurrencyInfoByExchangeName( - GetAllEnabledExchangeAccountInfo().Data, "ANX", - ) - if err == nil { - t.Error( - "Test Failed - Wallet_Routes_Test.go - GetAccountCurrencyInfoByExchangeName", - ) - } -} - -func TestGetAllEnabledExchangeAccountInfo(t *testing.T) { - if value := GetAllEnabledExchangeAccountInfo(); len(value.Data) != 0 { - t.Error( - "Test Failed - Wallet_Routes_Test.go - GetAllEnabledExchangeAccountInfo", - ) - } -} diff --git a/websocket.go b/websocket.go new file mode 100644 index 00000000..0d93dc56 --- /dev/null +++ b/websocket.go @@ -0,0 +1,349 @@ +package main + +import ( + "errors" + "log" + "net/http" + "time" + + "github.com/gorilla/websocket" + "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/config" + "github.com/thrasher-/gocryptotrader/currency" +) + +// Const vars for websocket +const ( + WebsocketResponseSuccess = "OK" +) + +// WebsocketClient stores information related to the websocket client +type WebsocketClient struct { + ID int + Conn *websocket.Conn + LastRecv time.Time + Authenticated bool +} + +// WebsocketEvent is the struct used for websocket events +type WebsocketEvent struct { + Exchange string `json:"exchange,omitempty"` + AssetType string `json:"assetType,omitempty"` + Event string + Data interface{} +} + +// WebsocketEventResponse is the struct used for websocket event responses +type WebsocketEventResponse struct { + Event string `json:"event"` + Data interface{} `json:"data"` + Error string `json:"error"` +} + +// WebsocketOrderbookTickerRequest is a struct used for ticker and orderbook +// requests +type WebsocketOrderbookTickerRequest struct { + Exchange string `json:"exchangeName"` + Currency string `json:"currency"` + AssetType string `json:"assetType"` +} + +// WebsocketClientHub stores an array of websocket clients +var WebsocketClientHub []WebsocketClient + +// WebsocketClientHandler upgrades the HTTP connection to a websocket +// compatible one +func WebsocketClientHandler(w http.ResponseWriter, r *http.Request) { + connectionLimit := bot.config.Webserver.WebsocketConnectionLimit + numClients := len(WebsocketClientHub) + + if numClients >= connectionLimit { + log.Printf("Websocket client rejected due to websocket client limit reached. Number of clients %d. Limit %d.", + numClients, connectionLimit) + w.WriteHeader(http.StatusForbidden) + return + } + + upgrader := websocket.Upgrader{ + WriteBufferSize: 1024, + ReadBufferSize: 1024, + } + + // Allow insecure origin if the Origin request header is present and not + // equal to the Host request header. Default to false + if bot.config.Webserver.WebsocketAllowInsecureOrigin { + upgrader.CheckOrigin = func(r *http.Request) bool { return true } + } + + newClient := WebsocketClient{ + ID: len(WebsocketClientHub), + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } + + newClient.Conn = conn + WebsocketClientHub = append(WebsocketClientHub, newClient) + numClients++ + log.Printf("New websocket client connected. Connected clients: %d. Limit %d.", + numClients, connectionLimit) +} + +// DisconnectWebsocketClient disconnects a websocket client +func DisconnectWebsocketClient(id int, err error) { + for i := range WebsocketClientHub { + if WebsocketClientHub[i].ID == id { + WebsocketClientHub[i].Conn.Close() + WebsocketClientHub = append(WebsocketClientHub[:i], WebsocketClientHub[i+1:]...) + log.Printf("Disconnected Websocket client, error: %s", err) + return + } + } +} + +// SendWebsocketMessage sends a websocket message to a specific client +func SendWebsocketMessage(id int, data interface{}) error { + for _, x := range WebsocketClientHub { + if x.ID == id { + return x.Conn.WriteJSON(data) + } + } + return nil +} + +// BroadcastWebsocketMessage broadcasts a websocket event message to all +// websocket clients +func BroadcastWebsocketMessage(evt WebsocketEvent) error { + for _, x := range WebsocketClientHub { + x.Conn.WriteJSON(evt) + } + return nil +} + +// WebsocketAuth is a struct used for +type WebsocketAuth struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type wsCommandHandler func(wsClient *websocket.Conn, data interface{}) error + +var wsHandlers = map[string]wsCommandHandler{ + "getconfig": wsGetConfig, + "saveconfig": wsSaveConfig, + "getaccountinfo": wsGetAccountInfo, + "gettickers": wsGetTickers, + "getticker": wsGetTicker, + "getorderbooks": wsGetOrderbooks, + "getorderbook": wsGetOrderbook, + "getexchangerates": wsGetExchangeRates, + "getportfolio": wsGetPortfolio, +} + +func wsGetConfig(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetConfig", + Data: bot.config, + } + return wsClient.WriteJSON(wsResp) +} + +func wsSaveConfig(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "SaveConfig", + } + var cfg config.Config + err := common.JSONDecode(data.([]byte), &cfg) + if err != nil { + wsResp.Error = err.Error() + err = wsClient.WriteJSON(wsResp) + if err != nil { + return err + } + } + + err = bot.config.UpdateConfig(bot.configFile, cfg) + if err != nil { + wsResp.Error = err.Error() + err = wsClient.WriteJSON(wsResp) + if err != nil { + return err + } + } + + setupBotExchanges() + wsResp.Data = WebsocketResponseSuccess + return wsClient.WriteJSON(wsResp) +} + +func wsGetAccountInfo(wsClient *websocket.Conn, data interface{}) error { + accountInfo := GetAllEnabledExchangeAccountInfo() + wsResp := WebsocketEventResponse{ + Event: "GetAccountInfo", + Data: accountInfo, + } + return wsClient.WriteJSON(wsResp) +} + +func wsGetTickers(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetTickers", + } + wsResp.Data = GetAllActiveTickers() + return wsClient.WriteJSON(wsResp) +} + +func wsGetTicker(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetTicker", + } + var tickerReq WebsocketOrderbookTickerRequest + err := common.JSONDecode(data.([]byte), &tickerReq) + if err != nil { + wsResp.Error = err.Error() + wsClient.WriteJSON(wsResp) + return err + } + + result, err := GetSpecificTicker(tickerReq.Currency, + tickerReq.Exchange, tickerReq.AssetType) + + if err != nil { + wsResp.Error = err.Error() + wsClient.WriteJSON(wsResp) + return err + } + wsResp.Data = result + return wsClient.WriteJSON(wsResp) +} + +func wsGetOrderbooks(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetOrderbooks", + } + wsResp.Data = GetAllActiveOrderbooks() + return wsClient.WriteJSON(wsResp) +} + +func wsGetOrderbook(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetOrderbook", + } + var orderbookReq WebsocketOrderbookTickerRequest + err := common.JSONDecode(data.([]byte), &orderbookReq) + if err != nil { + wsResp.Error = err.Error() + wsClient.WriteJSON(wsResp) + return err + } + + result, err := GetSpecificOrderbook(orderbookReq.Currency, + orderbookReq.Exchange, orderbookReq.AssetType) + + if err != nil { + wsResp.Error = err.Error() + wsClient.WriteJSON(wsResp) + return err + } + wsResp.Data = result + return wsClient.WriteJSON(wsResp) +} + +func wsGetExchangeRates(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetExchangeRates", + } + if currency.YahooEnabled { + wsResp.Data = currency.CurrencyStore + } else { + wsResp.Data = currency.CurrencyStoreFixer + } + return wsClient.WriteJSON(wsResp) +} + +func wsGetPortfolio(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetPortfolio", + } + wsResp.Data = bot.portfolio.GetPortfolioSummary() + return wsClient.WriteJSON(wsResp) +} + +// WebsocketHandler Handles websocket client requests +func WebsocketHandler() { + for { + for x := range WebsocketClientHub { + var evt WebsocketEvent + err := WebsocketClientHub[x].Conn.ReadJSON(&evt) + if err != nil { + DisconnectWebsocketClient(x, err) + continue + } + + if evt.Event == "" { + DisconnectWebsocketClient(x, errors.New("Websocket client sent data we did not understand")) + continue + } + + dataJSON, err := common.JSONEncode(evt.Data) + if err != nil { + log.Println(err) + continue + } + + req := common.StringToLower(evt.Event) + log.Printf("Websocket req: %s", req) + + if !WebsocketClientHub[x].Authenticated && evt.Event != "auth" { + wsResp := WebsocketEventResponse{ + Event: "auth", + Error: "you must authenticate first", + } + SendWebsocketMessage(x, wsResp) + DisconnectWebsocketClient(x, errors.New("Websocket client did not auth")) + continue + } else if !WebsocketClientHub[x].Authenticated && evt.Event == "auth" { + var auth WebsocketAuth + err = common.JSONDecode(dataJSON, &auth) + if err != nil { + log.Println(err) + continue + } + hashPW := common.HexEncodeToString(common.GetSHA256([]byte(bot.config.Webserver.AdminPassword))) + if auth.Username == bot.config.Webserver.AdminUsername && auth.Password == hashPW { + WebsocketClientHub[x].Authenticated = true + wsResp := WebsocketEventResponse{ + Event: "auth", + Data: WebsocketResponseSuccess, + } + SendWebsocketMessage(x, wsResp) + log.Println("Websocket client authenticated successfully") + continue + } else { + wsResp := WebsocketEventResponse{ + Event: "auth", + Error: "invalid username/password", + } + SendWebsocketMessage(x, wsResp) + DisconnectWebsocketClient(x, errors.New("Websocket client sent wrong username/password")) + continue + } + } + result, ok := wsHandlers[req] + if !ok { + log.Printf("Websocket unsupported event") + continue + } + + err = result(WebsocketClientHub[x].Conn, dataJSON) + if err != nil { + log.Printf("Websocket request %s failed. Error %s", evt.Event, err) + continue + } + } + time.Sleep(time.Millisecond) + } +}