From f6efa9ee372a09350e44d8c3d7289e1bea047706 Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Wed, 5 Apr 2017 15:49:09 +1000 Subject: [PATCH] Expand portfolio to cover exchange balances --- exchanges/bitfinex/bitfinex_wrapper.go | 2 +- exchanges/bitstamp/bitstamp_wrapper.go | 31 ++++++--- exchanges/exchange.go | 1 + exchanges/gdax/gdax_wrapper.go | 2 +- exchanges/okcoin/okcoin_types.go | 3 + exchanges/okcoin/okcoin_wrapper.go | 31 ++++++++- exchanges/poloniex/poloniex_wrapper.go | 8 +-- main.go | 30 +++++++++ portfolio/portfolio.go | 87 ++++++++++++++++++++++++-- wallet_routes.go | 42 ++++++++++++- 10 files changed, 212 insertions(+), 25 deletions(-) diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index eb32d3ad..a48e54a8 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -88,7 +88,7 @@ func (e *Bitfinex) GetExchangeAccountInfo() (exchange.ExchangeAccountInfo, error for i := 0; i < len(accountBalance); i++ { var exchangeCurrency exchange.ExchangeAccountCurrencyInfo - exchangeCurrency.CurrencyName = accountBalance[i].Currency + exchangeCurrency.CurrencyName = common.StringToUpper(accountBalance[i].Currency) exchangeCurrency.TotalValue = accountBalance[i].Amount exchangeCurrency.Hold = accountBalance[i].Available diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index 7f3d9124..58a1b7cf 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -75,17 +75,28 @@ func (e *Bitstamp) GetExchangeAccountInfo() (exchange.ExchangeAccountInfo, error return response, err } - var btcExchangeInfo exchange.ExchangeAccountCurrencyInfo - btcExchangeInfo.CurrencyName = "BTC" - btcExchangeInfo.TotalValue = accountBalance.BTCBalance - btcExchangeInfo.Hold = accountBalance.BTCReserved - response.Currencies = append(response.Currencies, btcExchangeInfo) + response.Currencies = append(response.Currencies, exchange.ExchangeAccountCurrencyInfo{ + CurrencyName: "BTC", + TotalValue: accountBalance.BTCAvailable, + Hold: accountBalance.BTCReserved, + }) - var usdExchangeInfo exchange.ExchangeAccountCurrencyInfo - usdExchangeInfo.CurrencyName = "USD" - usdExchangeInfo.TotalValue = accountBalance.USDBalance - usdExchangeInfo.Hold = accountBalance.USDReserved - response.Currencies = append(response.Currencies, usdExchangeInfo) + response.Currencies = append(response.Currencies, exchange.ExchangeAccountCurrencyInfo{ + CurrencyName: "XRP", + TotalValue: accountBalance.XRPAvailable, + Hold: accountBalance.XRPReserved, + }) + response.Currencies = append(response.Currencies, exchange.ExchangeAccountCurrencyInfo{ + CurrencyName: "USD", + TotalValue: accountBalance.USDAvailable, + Hold: accountBalance.USDReserved, + }) + + response.Currencies = append(response.Currencies, exchange.ExchangeAccountCurrencyInfo{ + CurrencyName: "EUR", + TotalValue: accountBalance.EURAvailable, + Hold: accountBalance.EURReserved, + }) return response, nil } diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 8b09d2a0..162a7228 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -11,6 +11,7 @@ import ( const ( WarningBase64DecryptSecretKeyFailed = "WARNING -- Exchange %s unable to base64 decode secret key.. Disabling Authenticated API support." + ErrExchangeNotFound = "Exchange not found in dataset." ) //ExchangeAccountInfo : Generic type to hold each exchange's holdings in all enabled currencies diff --git a/exchanges/gdax/gdax_wrapper.go b/exchanges/gdax/gdax_wrapper.go index a99d8290..6b30b674 100644 --- a/exchanges/gdax/gdax_wrapper.go +++ b/exchanges/gdax/gdax_wrapper.go @@ -70,7 +70,7 @@ func (e *GDAX) GetExchangeAccountInfo() (exchange.ExchangeAccountInfo, error) { for i := 0; i < len(accountBalance); i++ { var exchangeCurrency exchange.ExchangeAccountCurrencyInfo exchangeCurrency.CurrencyName = accountBalance[i].Currency - exchangeCurrency.TotalValue = accountBalance[i].Balance + exchangeCurrency.TotalValue = accountBalance[i].Available exchangeCurrency.Hold = accountBalance[i].Hold response.Currencies = append(response.Currencies, exchangeCurrency) diff --git a/exchanges/okcoin/okcoin_types.go b/exchanges/okcoin/okcoin_types.go index 0d279a01..b8c338d3 100644 --- a/exchanges/okcoin/okcoin_types.go +++ b/exchanges/okcoin/okcoin_types.go @@ -131,16 +131,19 @@ type OKCoinUserInfo struct { BTC float64 `json:"btc,string"` LTC float64 `json:"ltc,string"` USD float64 `json:"usd,string"` + CNY float64 `json:"cny,string"` } `json:"borrow"` Free struct { BTC float64 `json:"btc,string"` LTC float64 `json:"ltc,string"` USD float64 `json:"usd,string"` + CNY float64 `json:"cny,string"` } `json:"free"` Freezed struct { BTC float64 `json:"btc,string"` LTC float64 `json:"ltc,string"` USD float64 `json:"usd,string"` + CNY float64 `json:"cny,string"` } `json:"freezed"` UnionFund struct { BTC float64 `json:"btc,string"` diff --git a/exchanges/okcoin/okcoin_wrapper.go b/exchanges/okcoin/okcoin_wrapper.go index 46ec5618..e680ea69 100644 --- a/exchanges/okcoin/okcoin_wrapper.go +++ b/exchanges/okcoin/okcoin_wrapper.go @@ -94,10 +94,37 @@ func (o *OKCoin) GetTickerPrice(currency string) (ticker.TickerPrice, error) { return tickerPrice, nil } -//TODO support for retrieving holdings from OKCOIN -//GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the OKCoin exchange func (e *OKCoin) GetExchangeAccountInfo() (exchange.ExchangeAccountInfo, error) { var response exchange.ExchangeAccountInfo response.ExchangeName = e.GetName() + assets, err := e.GetUserInfo() + if err != nil { + return response, err + } + + response.Currencies = append(response.Currencies, exchange.ExchangeAccountCurrencyInfo{ + CurrencyName: "BTC", + TotalValue: assets.Info.Funds.Free.BTC, + Hold: assets.Info.Funds.Freezed.BTC, + }) + + response.Currencies = append(response.Currencies, exchange.ExchangeAccountCurrencyInfo{ + CurrencyName: "LTC", + TotalValue: assets.Info.Funds.Free.LTC, + Hold: assets.Info.Funds.Freezed.LTC, + }) + + response.Currencies = append(response.Currencies, exchange.ExchangeAccountCurrencyInfo{ + CurrencyName: "USD", + TotalValue: assets.Info.Funds.Free.USD, + Hold: assets.Info.Funds.Freezed.USD, + }) + + response.Currencies = append(response.Currencies, exchange.ExchangeAccountCurrencyInfo{ + CurrencyName: "CNY", + TotalValue: assets.Info.Funds.Free.CNY, + Hold: assets.Info.Funds.Freezed.CNY, + }) + return response, nil } diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 9803e97b..0f2f6574 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -76,11 +76,11 @@ func (e *Poloniex) GetExchangeAccountInfo() (exchange.ExchangeAccountInfo, error if err != nil { return response, err } - currencies := e.AvailablePairs - for i := 0; i < len(currencies); i++ { + + for x, y := range accountBalance.Currency { var exchangeCurrency exchange.ExchangeAccountCurrencyInfo - exchangeCurrency.CurrencyName = currencies[i] - exchangeCurrency.TotalValue = accountBalance.Currency[currencies[i]] + exchangeCurrency.CurrencyName = x + exchangeCurrency.TotalValue = y response.Currencies = append(response.Currencies, exchangeCurrency) } return response, nil diff --git a/main.go b/main.go index ab5c668c..3aba0a7b 100644 --- a/main.go +++ b/main.go @@ -151,6 +151,7 @@ func main() { bot.portfolio = &portfolio.Portfolio bot.portfolio.SeedPortfolio(bot.config.Portfolio) + SeedExchangeAccountInfo(GetAllEnabledExchangeAccountInfo().Data) go portfolio.StartPortfolioWatcher() if bot.config.Webserver.Enabled { @@ -217,3 +218,32 @@ func Shutdown() { log.Println("Exiting.") os.Exit(1) } + +func SeedExchangeAccountInfo(data []exchange.ExchangeAccountInfo) { + if len(data) == 0 { + return + } + + port := portfolio.GetPortfolio() + + for i := 0; i < len(data); i++ { + exchangeName := data[i].ExchangeName + for j := 0; j < len(data[i].Currencies); j++ { + currencyName := data[i].Currencies[j].CurrencyName + onHold := data[i].Currencies[j].Hold + avail := data[i].Currencies[j].TotalValue + total := onHold + avail + + if total <= 0 { + continue + } + + if !port.ExchangeAddressExists(exchangeName, currencyName) { + port.Addresses = append(port.Addresses, portfolio.PortfolioAddress{Address: exchangeName, CoinType: currencyName, Balance: total, Decscription: portfolio.PORTFOLIO_ADDRESS_EXCHANGE}) + } else { + port.UpdateExchangeAddressBalance(exchangeName, currencyName, total) + } + } + } + +} diff --git a/portfolio/portfolio.go b/portfolio/portfolio.go index f28037bc..967649d8 100644 --- a/portfolio/portfolio.go +++ b/portfolio/portfolio.go @@ -23,9 +23,10 @@ const ( var Portfolio PortfolioBase type PortfolioAddress struct { - Address string - CoinType string - Balance float64 + Address string + CoinType string + Balance float64 + Decscription string } type PortfolioBase struct { @@ -65,6 +66,19 @@ type EtherchainBalanceResponse struct { } `json:"data"` } +//ExchangeAccountInfo : Generic type to hold each exchange's holdings in all enabled currencies +type ExchangeAccountInfo struct { + ExchangeName string + Currencies []ExchangeAccountCurrencyInfo +} + +//ExchangeAccountCurrencyInfo : Sub type to store currency name and value +type ExchangeAccountCurrencyInfo struct { + CurrencyName string + TotalValue float64 + Hold float64 +} + func GetEthereumBalance(address []string) (EtherchainBalanceResponse, error) { addresses := common.JoinStrings(address, ",") url := fmt.Sprintf("%s/%s/%s", ETHERCHAIN_API_URL, ETHERCHAIN_ACCOUNT_MULTIPLE, addresses) @@ -115,6 +129,15 @@ func (p *PortfolioBase) GetAddressBalance(address string) (float64, bool) { return 0, false } +func (p *PortfolioBase) ExchangeExists(exchangeName string) bool { + for _, x := range p.Addresses { + if x.Address == exchangeName { + return true + } + } + return false +} + func (p *PortfolioBase) AddressExists(address string) bool { for _, x := range p.Addresses { if x.Address == address { @@ -124,6 +147,15 @@ func (p *PortfolioBase) AddressExists(address string) bool { return false } +func (p *PortfolioBase) ExchangeAddressExists(exchangeName, coinType string) bool { + for _, x := range p.Addresses { + if x.Address == exchangeName && x.CoinType == coinType { + return true + } + } + return false +} + func (p *PortfolioBase) UpdateAddressBalance(address string, amount float64) { for x, _ := range p.Addresses { if p.Addresses[x].Address == address { @@ -132,7 +164,19 @@ func (p *PortfolioBase) UpdateAddressBalance(address string, amount float64) { } } +func (p *PortfolioBase) UpdateExchangeAddressBalance(exchangeName, coinType string, balance float64) { + for x, _ := range p.Addresses { + if p.Addresses[x].Address == exchangeName && p.Addresses[x].CoinType == coinType { + p.Addresses[x].Balance = balance + } + } +} + func (p *PortfolioBase) UpdatePortfolio(addresses []string, coinType string) bool { + if common.StringContains(common.JoinStrings(addresses, ","), PORTFOLIO_ADDRESS_EXCHANGE) || common.StringContains(common.JoinStrings(addresses, ","), PORTFOLIO_ADDRESS_CUSTOM) { + return true + } + if coinType == "ETH" { result, err := GetEthereumBalance(addresses) if err != nil { @@ -174,6 +218,38 @@ func (p *PortfolioBase) UpdatePortfolio(addresses []string, coinType string) boo return true } +func (p *PortfolioBase) GetExchangePortfolio() map[string]float64 { + result := make(map[string]float64) + for _, x := range p.Addresses { + if x.Decscription != PORTFOLIO_ADDRESS_EXCHANGE { + continue + } + balance, ok := result[x.CoinType] + if !ok { + result[x.CoinType] = x.Balance + } else { + result[x.CoinType] = x.Balance + balance + } + } + return result +} + +func (p *PortfolioBase) GetPersonalPortfolio() map[string]float64 { + result := make(map[string]float64) + for _, x := range p.Addresses { + if x.Decscription == PORTFOLIO_ADDRESS_EXCHANGE { + continue + } + balance, ok := result[x.CoinType] + if !ok { + result[x.CoinType] = x.Balance + } else { + result[x.CoinType] = x.Balance + balance + } + } + return result +} + func (p *PortfolioBase) GetPortfolioSummary(coinFilter string) map[string]float64 { result := make(map[string]float64) for _, x := range p.Addresses { @@ -193,6 +269,9 @@ func (p *PortfolioBase) GetPortfolioSummary(coinFilter string) map[string]float6 func (p *PortfolioBase) GetPortfolioGroupedCoin() map[string][]string { result := make(map[string][]string) for _, x := range p.Addresses { + if common.StringContains(x.Decscription, PORTFOLIO_ADDRESS_EXCHANGE) || common.StringContains(x.Decscription, PORTFOLIO_ADDRESS_CUSTOM) { + continue + } result[x.CoinType] = append(result[x.CoinType], x.Address) } return result @@ -204,7 +283,7 @@ func (p *PortfolioBase) SeedPortfolio(port PortfolioBase) { func StartPortfolioWatcher() { addrCount := len(Portfolio.Addresses) - log.Printf("PortfolioWatcher started: Have %d address(es) in portfolio.\n", addrCount) + log.Printf("PortfolioWatcher started: Have %d entries in portfolio.\n", addrCount) for { data := Portfolio.GetPortfolioGroupedCoin() for key, value := range data { diff --git a/wallet_routes.go b/wallet_routes.go index 9937e90b..7efa0298 100644 --- a/wallet_routes.go +++ b/wallet_routes.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "errors" "log" "net/http" @@ -12,9 +13,39 @@ type AllEnabledExchangeAccounts struct { Data []exchange.ExchangeAccountInfo `json:"data"` } -func GetAllEnabledAccountInfo(w http.ResponseWriter, r *http.Request) { - var response AllEnabledExchangeAccounts +func GetCollatedExchangeAccountInfoByCoin(accounts []exchange.ExchangeAccountInfo) map[string]exchange.ExchangeAccountCurrencyInfo { + result := make(map[string]exchange.ExchangeAccountCurrencyInfo) + 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.ExchangeAccountCurrencyInfo{CurrencyName: currencyName, Hold: onHold, TotalValue: avail} + result[currencyName] = accountInfo + } else { + info.Hold += onHold + info.TotalValue += avail + result[currencyName] = info + } + } + } + return result +} + +func GetAccountCurrencyInfoByExchangeName(accounts []exchange.ExchangeAccountInfo, exchangeName string) (exchange.ExchangeAccountInfo, error) { + for i := 0; i < len(accounts); i++ { + if accounts[i].ExchangeName == exchangeName { + return accounts[i], nil + } + } + return exchange.ExchangeAccountInfo{}, errors.New(exchange.ErrExchangeNotFound) +} + +func GetAllEnabledExchangeAccountInfo() AllEnabledExchangeAccounts { + var response AllEnabledExchangeAccounts for _, individualBot := range bot.exchanges { if individualBot != nil && individualBot.IsEnabled() { individualExchange, err := individualBot.GetExchangeAccountInfo() @@ -24,6 +55,11 @@ func GetAllEnabledAccountInfo(w http.ResponseWriter, r *http.Request) { response.Data = append(response.Data, individualExchange) } } + return response +} + +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 { @@ -36,6 +72,6 @@ var WalletRoutes = Routes{ "AllEnabledAccountInfo", "GET", "/exchanges/enabled/accounts/all", - GetAllEnabledAccountInfo, + SendAllEnabledAccountInfo, }, }