From 4f34b58d5557ff4fa1eb785829d3e613d7ca1c27 Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Thu, 17 Aug 2017 11:56:54 +1000 Subject: [PATCH] Improve portfolio, coverage and tool --- currency/currency.go | 13 +- exchanges/bitfinex/bitfinex_wrapper.go | 41 ++++- exchanges/btcmarkets/btcmarkets.go | 7 +- main.go | 19 +- portfolio/portfolio.go | 230 ++++++++++++++++++++++--- portfolio/portfolio_test.go | 146 ++++++++++++++-- portfolio_routes.go | 27 +++ restful_router.go | 1 + tools/portfolio/portfolio.go | 164 ++++++++++++------ 9 files changed, 532 insertions(+), 116 deletions(-) create mode 100644 portfolio_routes.go diff --git a/currency/currency.go b/currency/currency.go index a89c46ba..794cea42 100644 --- a/currency/currency.go +++ b/currency/currency.go @@ -197,19 +197,20 @@ func MakecurrencyPairs(supportedCurrencies string) string { // or vice versa. func ConvertCurrency(amount float64, from, to string) (float64, error) { currency := common.StringToUpper(from + to) - if CurrencyStore[currency].Name != currency { + + _, ok := CurrencyStore[currency] + if !ok { err := SeedCurrencyData(currency[:len(from)] + "," + currency[len(to):]) if err != nil { return 0, err } } - for x, y := range CurrencyStore { - if x == currency { - return amount * y.Rate, nil - } + result, ok := CurrencyStore[currency] + if !ok { + return 0, ErrCurrencyNotFound } - return 0, ErrCurrencyNotFound + return amount * result.Rate, nil } // FetchYahooCurrencyData seeds the variable CurrencyStore; this is a diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 935124a3..75d26feb 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -106,24 +106,47 @@ func (b *Bitfinex) GetOrderbookEx(p pair.CurrencyPair) (orderbook.OrderbookBase, } //GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the Bitfinex exchange -func (e *Bitfinex) GetExchangeAccountInfo() (exchange.AccountInfo, error) { +func (b *Bitfinex) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.ExchangeName = e.GetName() - accountBalance, err := e.GetAccountBalance() + response.ExchangeName = b.GetName() + accountBalance, err := b.GetAccountBalance() if err != nil { return response, err } - if !e.Enabled { + if !b.Enabled { return response, nil } - for i := 0; i < len(accountBalance); i++ { - var exchangeCurrency exchange.AccountCurrencyInfo - exchangeCurrency.CurrencyName = common.StringToUpper(accountBalance[i].Currency) - exchangeCurrency.TotalValue = accountBalance[i].Amount - exchangeCurrency.Hold = accountBalance[i].Available + type bfxCoins struct { + OnHold float64 + Available float64 + } + accounts := make(map[string]bfxCoins) + + for i := range accountBalance { + onHold := accountBalance[i].Amount - accountBalance[i].Available + coins := bfxCoins{ + OnHold: onHold, + Available: accountBalance[i].Available, + } + result, ok := accounts[accountBalance[i].Currency] + if !ok { + accounts[accountBalance[i].Currency] = coins + } else { + result.Available += accountBalance[i].Available + result.OnHold += onHold + accounts[accountBalance[i].Currency] = result + } + } + + for x, y := range accounts { + var exchangeCurrency exchange.AccountCurrencyInfo + exchangeCurrency.CurrencyName = common.StringToUpper(x) + exchangeCurrency.TotalValue = y.Available + y.OnHold + exchangeCurrency.Hold = y.OnHold response.Currencies = append(response.Currencies, exchangeCurrency) } + return response, nil } diff --git a/exchanges/btcmarkets/btcmarkets.go b/exchanges/btcmarkets/btcmarkets.go index 1f04de50..f68d8eac 100644 --- a/exchanges/btcmarkets/btcmarkets.go +++ b/exchanges/btcmarkets/btcmarkets.go @@ -272,11 +272,10 @@ func (b *BTCMarkets) GetAccountBalance() ([]BTCMarketsAccountBalance, error) { return nil, err } + // All values are returned in Satoshis, even for fiat currencies. for i := range balance { - if balance[i].Currency == "LTC" || balance[i].Currency == "BTC" { - balance[i].Balance = balance[i].Balance / common.SatoshisPerBTC - balance[i].PendingFunds = balance[i].PendingFunds / common.SatoshisPerBTC - } + balance[i].Balance = balance[i].Balance / common.SatoshisPerBTC + balance[i].PendingFunds = balance[i].PendingFunds / common.SatoshisPerBTC } return balance, nil } diff --git a/main.go b/main.go index e91ef439..935b8121 100644 --- a/main.go +++ b/main.go @@ -273,18 +273,27 @@ func SeedExchangeAccountInfo(data []exchange.AccountInfo) { avail := data[i].Currencies[j].TotalValue total := onHold + avail - if total <= 0 { - continue - } - if !port.ExchangeAddressExists(exchangeName, currencyName) { + if total <= 0 { + continue + } + log.Printf("Portfolio: Adding new exchange address: %s, %s, %f, %s\n", + exchangeName, currencyName, total, portfolio.PortfolioAddressExchange) port.Addresses = append( port.Addresses, portfolio.Address{Address: exchangeName, CoinType: currencyName, Balance: total, Description: portfolio.PortfolioAddressExchange}, ) } else { - port.UpdateExchangeAddressBalance(exchangeName, currencyName, total) + if total <= 0 { + log.Printf("Portfolio: Removing %s %s entry.\n", exchangeName, + 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) + } } } } diff --git a/portfolio/portfolio.go b/portfolio/portfolio.go index 1de4676d..ddff2170 100644 --- a/portfolio/portfolio.go +++ b/portfolio/portfolio.go @@ -16,9 +16,10 @@ const ( etherchainAPIURL = "https://etherchain.org/api" etherchainAccountMultiple = "account/multiple" - // PortfolioAddressExchange holds the current portfolio address + // PortfolioAddressExchange is a label for an exchange address PortfolioAddressExchange = "Exchange" - portfolioAddressPersonal = "Personal" + // PortfolioAddressPersonal is a label for a personal/offline address + PortfolioAddressPersonal = "Personal" ) // Portfolio is variable store holding an array of portfolioAddress @@ -172,9 +173,9 @@ func GetBlockrAddressMulti(addresses []string, coinType string) (BlockrAddressBa // GetAddressBalance acceses the portfolio base and returns the balance by passed // in address func (p *Base) GetAddressBalance(address string) (float64, bool) { - for _, x := range p.Addresses { - if x.Address == address { - return x.Balance, true + for x := range p.Addresses { + if p.Addresses[x].Address == address { + return p.Addresses[x].Balance, true } } return 0, false @@ -182,8 +183,8 @@ func (p *Base) GetAddressBalance(address string) (float64, bool) { // ExchangeExists checks to see if an exchange exists in the portfolio base func (p *Base) ExchangeExists(exchangeName string) bool { - for _, x := range p.Addresses { - if x.Address == exchangeName { + for x := range p.Addresses { + if p.Addresses[x].Address == exchangeName { return true } } @@ -193,8 +194,8 @@ func (p *Base) ExchangeExists(exchangeName string) bool { // AddressExists checks to see if there is an address associated with the // portfolio base func (p *Base) AddressExists(address string) bool { - for _, x := range p.Addresses { - if x.Address == address { + for x := range p.Addresses { + if p.Addresses[x].Address == address { return true } } @@ -204,8 +205,8 @@ func (p *Base) AddressExists(address string) bool { // ExchangeAddressExists checks to see if there is an exchange address // associated with the portfolio base func (p *Base) ExchangeAddressExists(exchangeName, coinType string) bool { - for _, x := range p.Addresses { - if x.Address == exchangeName && x.CoinType == coinType { + for x := range p.Addresses { + if p.Addresses[x].Address == exchangeName && p.Addresses[x].CoinType == coinType { return true } } @@ -221,6 +222,16 @@ func (p *Base) UpdateAddressBalance(address string, amount float64) { } } +// RemoveExchangeAddress removes an exchange address from the portfolio. +func (p *Base) RemoveExchangeAddress(exchangeName, coinType string) { + for x := range p.Addresses { + if p.Addresses[x].Address == exchangeName && p.Addresses[x].CoinType == coinType { + p.Addresses = append(p.Addresses[:x], p.Addresses[x+1:]...) + return + } + } +} + // UpdateExchangeAddressBalance updates the portfolio balance when checked // against correct exchangeName and coinType. func (p *Base) UpdateExchangeAddressBalance(exchangeName, coinType string, balance float64) { @@ -239,13 +250,28 @@ func (p *Base) AddAddress(address, coinType, description string, balance float64 Balance: balance, Description: description}, ) } else { - p.UpdateAddressBalance(address, balance) + if balance <= 0 { + p.RemoveAddress(address, coinType, description) + } else { + p.UpdateAddressBalance(address, balance) + } + } +} + +// RemoveAddress removes an address when checked against the correct address and +// coinType +func (p *Base) RemoveAddress(address, coinType, description string) { + for x := range p.Addresses { + if p.Addresses[x].Address == address && p.Addresses[x].CoinType == coinType && p.Addresses[x].Description == description { + p.Addresses = append(p.Addresses[:x], p.Addresses[x+1:]...) + return + } } } // UpdatePortfolio adds to the portfolio addresses by coin type func (p *Base) UpdatePortfolio(addresses []string, coinType string) bool { - if common.StringContains(common.JoinStrings(addresses, ","), PortfolioAddressExchange) || common.StringContains(common.JoinStrings(addresses, ","), portfolioAddressPersonal) { + if common.StringContains(common.JoinStrings(addresses, ","), PortfolioAddressExchange) || common.StringContains(common.JoinStrings(addresses, ","), PortfolioAddressPersonal) { return true } @@ -256,7 +282,7 @@ func (p *Base) UpdatePortfolio(addresses []string, coinType string) bool { } for _, x := range result.Data { - p.AddAddress(x.Address, coinType, portfolioAddressPersonal, x.Balance) + p.AddAddress(x.Address, coinType, PortfolioAddressPersonal, x.Balance) } return true } @@ -266,7 +292,7 @@ func (p *Base) UpdatePortfolio(addresses []string, coinType string) bool { return false } for _, x := range result.Data { - p.AddAddress(x.Address, coinType, portfolioAddressPersonal, x.Balance) + p.AddAddress(x.Address, coinType, PortfolioAddressPersonal, x.Balance) } } else { result, err := GetBlockrBalanceSingle(addresses[0], coinType) @@ -274,12 +300,23 @@ func (p *Base) UpdatePortfolio(addresses []string, coinType string) bool { return false } p.AddAddress( - addresses[0], coinType, portfolioAddressPersonal, result.Data.Balance, + addresses[0], coinType, PortfolioAddressPersonal, result.Data.Balance, ) } return true } +// GetPortfolioByExchange returns currency portfolio amount by exchange +func (p *Base) GetPortfolioByExchange(exchangeName string) map[string]float64 { + result := make(map[string]float64) + for x := range p.Addresses { + if common.StringContains(p.Addresses[x].Address, exchangeName) { + result[p.Addresses[x].CoinType] = p.Addresses[x].Balance + } + } + return result +} + // GetExchangePortfolio returns current portfolio base information func (p *Base) GetExchangePortfolio() map[string]float64 { result := make(map[string]float64) @@ -314,28 +351,167 @@ func (p *Base) GetPersonalPortfolio() map[string]float64 { return result } -// GetPortfolioSummary rpoves a summary for your portfolio base -func (p *Base) GetPortfolioSummary(coinFilter string) map[string]float64 { - result := make(map[string]float64) - for _, x := range p.Addresses { - if coinFilter != "" && coinFilter != x.CoinType { - continue +// getPercentage returns the percentage of the target coin amount against the +// total coin amount. +func getPercentage(input map[string]float64, target string, totals map[string]float64) float64 { + subtotal, _ := input[target] + total, _ := totals[target] + percentage := (subtotal / total) * 100 / 1 + return percentage +} + +// getPercentage returns the percentage a specific value of a target coin amount +// against the total coin amount. +func getPercentageSpecific(input float64, target string, totals map[string]float64) float64 { + total, _ := totals[target] + percentage := (input / total) * 100 / 1 + return percentage +} + +// Coin stores a coin type, balance, address and percentage relative to the total +// amount. +type Coin struct { + Coin string `json:"coin"` + Balance float64 `json:"balance"` + Address string `json:"address,omitempty"` + Percentage float64 `json:"percentage,omitempty"` +} + +// OfflineCoinSummary stores a coin types address, balance and percentage +// relative to the total amount. +type OfflineCoinSummary struct { + Address string `json:"address"` + Balance float64 `json:"balance"` + Percentage float64 `json:"percentage,omitempty"` +} + +// OnlineCoinSummary stores a coin types balance and percentage relative to the +// total amount. +type OnlineCoinSummary struct { + Balance float64 `json:"balance"` + Percentage float64 `json:"percentage,omitempty"` +} + +// Summary Stores the entire portfolio summary +type Summary struct { + Totals []Coin `json:"coin_totals"` + Offline []Coin `json:"coins_offline"` + OfflineSummary map[string][]OfflineCoinSummary `json:"offline_summary"` + Online []Coin `json:"coins_online"` + OnlineSummary map[string]map[string]OnlineCoinSummary `json:"online_summary"` +} + +// GetPortfolioSummary returns the complete portfolio summary, showing +// coin totals, offline and online summaries with their relative percentages. +func (p *Base) GetPortfolioSummary() Summary { + personalHoldings := p.GetPersonalPortfolio() + exchangeHoldings := p.GetExchangePortfolio() + totalCoins := make(map[string]float64) + + for x, y := range personalHoldings { + if x == "ETH" { + y = y / common.WeiPerEther + personalHoldings[x] = y } - balance, ok := result[x.CoinType] + balance, ok := totalCoins[x] if !ok { - result[x.CoinType] = x.Balance + totalCoins[x] = y } else { - result[x.CoinType] = x.Balance + balance + totalCoins[x] = y + balance } } - return result + + for x, y := range exchangeHoldings { + balance, ok := totalCoins[x] + if !ok { + totalCoins[x] = y + } else { + totalCoins[x] = y + balance + } + } + + var portfolioOutput Summary + for x, y := range totalCoins { + coins := Coin{Coin: x, Balance: y} + portfolioOutput.Totals = append(portfolioOutput.Totals, coins) + } + + for x, y := range personalHoldings { + coins := Coin{ + Coin: x, + Balance: y, + Percentage: getPercentage(personalHoldings, x, totalCoins), + } + portfolioOutput.Offline = append(portfolioOutput.Offline, coins) + } + + for x, y := range exchangeHoldings { + coins := Coin{ + Coin: x, + Balance: y, + Percentage: getPercentage(exchangeHoldings, x, totalCoins), + } + portfolioOutput.Online = append(portfolioOutput.Online, coins) + } + + var portfolioExchanges []string + for _, x := range p.Addresses { + if x.Description == PortfolioAddressExchange { + if !common.DataContains(portfolioExchanges, x.Address) { + portfolioExchanges = append(portfolioExchanges, x.Address) + } + } + } + + exchangeSummary := make(map[string]map[string]OnlineCoinSummary) + for x := range portfolioExchanges { + exchgName := portfolioExchanges[x] + result := p.GetPortfolioByExchange(exchgName) + + coinSummary := make(map[string]OnlineCoinSummary) + for y, z := range result { + coinSum := OnlineCoinSummary{ + Balance: z, + Percentage: getPercentageSpecific(z, y, totalCoins), + } + coinSummary[y] = coinSum + + } + exchangeSummary[exchgName] = coinSummary + } + portfolioOutput.OnlineSummary = exchangeSummary + + offlineSummary := make(map[string][]OfflineCoinSummary) + for _, x := range p.Addresses { + if x.Description != PortfolioAddressExchange { + if x.CoinType == "ETH" { + x.Balance = x.Balance / common.WeiPerEther + } + coinSummary := OfflineCoinSummary{ + Address: x.Address, + Balance: x.Balance, + Percentage: getPercentageSpecific(x.Balance, x.CoinType, + totalCoins), + } + result, ok := offlineSummary[x.CoinType] + if !ok { + offlineSummary[x.CoinType] = append(offlineSummary[x.CoinType], + coinSummary) + } else { + result = append(result, coinSummary) + offlineSummary[x.CoinType] = result + } + } + } + portfolioOutput.OfflineSummary = offlineSummary + return portfolioOutput } // GetPortfolioGroupedCoin returns portfolio base information grouped by coin func (p *Base) GetPortfolioGroupedCoin() map[string][]string { result := make(map[string][]string) for _, x := range p.Addresses { - if common.StringContains(x.Description, PortfolioAddressExchange) || common.StringContains(x.Description, portfolioAddressPersonal) { + if common.StringContains(x.Description, PortfolioAddressExchange) { continue } result[x.CoinType] = append(result[x.CoinType], x.Address) diff --git a/portfolio/portfolio_test.go b/portfolio/portfolio_test.go index c815a5ab..3a966481 100644 --- a/portfolio/portfolio_test.go +++ b/portfolio/portfolio_test.go @@ -153,12 +153,43 @@ func TestUpdateAddressBalance(t *testing.T) { newbase.AddAddress("someaddress", "LTC", "LTCWALLETTEST", 0.02) newbase.UpdateAddressBalance("someaddress", 0.03) - value := newbase.GetPortfolioSummary("LTC") - if value["LTC"] != 0.03 { + value := newbase.GetPortfolioSummary() + if value.Totals[0].Coin != "LTC" && value.Totals[0].Balance != 0.03 { t.Error("Test Failed - portfolio_test.go - UpdateUpdateAddressBalance error") } } +func TestRemoveAddress(t *testing.T) { + newbase := Base{} + newbase.AddAddress("someaddr", "LTC", "LTCWALLETTEST", 420) + + if !newbase.AddressExists("someaddr") { + t.Error("Test failed - portfolio_test.go - TestRemoveAddress") + } + + newbase.RemoveAddress("someaddr", "LTC", "LTCWALLETTEST") + if newbase.AddressExists("someaddr") { + t.Error("Test failed - portfolio_test.go - TestRemoveAddress") + } +} + +func TestRemoveExchangeAddress(t *testing.T) { + newbase := Base{} + exchangeName := "BallerExchange" + coinType := "LTC" + + newbase.AddAddress(exchangeName, coinType, PortfolioAddressExchange, 420) + + if !newbase.ExchangeAddressExists(exchangeName, coinType) { + t.Error("Test failed - portfolio_test.go - TestRemoveAddress") + } + + newbase.RemoveExchangeAddress(exchangeName, coinType) + if newbase.ExchangeAddressExists(exchangeName, coinType) { + t.Error("Test failed - portfolio_test.go - TestRemoveAddress") + } +} + func TestUpdateExchangeAddressBalance(t *testing.T) { newbase := Base{} newbase.AddAddress("someaddress", "LTC", "LTCWALLETTEST", 0.02) @@ -166,18 +197,25 @@ func TestUpdateExchangeAddressBalance(t *testing.T) { portfolio.SeedPortfolio(newbase) portfolio.UpdateExchangeAddressBalance("someaddress", "LTC", 0.04) - value := portfolio.GetPortfolioSummary("LTC") - if value["LTC"] != 0.04 { + value := portfolio.GetPortfolioSummary() + if value.Totals[0].Coin != "LTC" && value.Totals[0].Balance != 0.04 { t.Error("Test Failed - portfolio_test.go - UpdateExchangeAddressBalance error") } } func TestAddAddress(t *testing.T) { newbase := Base{} - newbase.AddAddress("someaddress", "LTC", "LTCWALLETTEST", 0.02) + newbase.AddAddress("Gibson", "LTC", "LTCWALLETTEST", 0.02) portfolio := GetPortfolio() portfolio.SeedPortfolio(newbase) - if !portfolio.AddressExists("someaddress") { + if !portfolio.AddressExists("Gibson") { + t.Error("Test Failed - portfolio_test.go - AddAddress error") + } + + // Test updating balance to <= 0, expected result is to remove the address. + // Fail if address still exists. + newbase.AddAddress("Gibson", "LTC", "LTCWALLETTEST", -1) + if newbase.AddressExists("Gibson") { t.Error("Test Failed - portfolio_test.go - AddAddress error") } } @@ -224,50 +262,128 @@ func TestUpdatePortfolio(t *testing.T) { if value { t.Error("Test Failed - portfolio_test.go - UpdatePortfolio error") } + + value = portfolio.UpdatePortfolio( + []string{PortfolioAddressExchange, PortfolioAddressPersonal}, "LTC") + + if !value { + t.Error("Test Failed - portfolio_test.go - UpdatePortfolio error") + } +} + +func TestGetPortfolioByExchange(t *testing.T) { + newbase := Base{} + newbase.AddAddress("ANX", "LTC", PortfolioAddressExchange, 0.07) + newbase.AddAddress("Bitfinex", "LTC", PortfolioAddressExchange, 0.05) + newbase.AddAddress("someaddress", "LTC", PortfolioAddressPersonal, 0.03) + portfolio := GetPortfolio() + portfolio.SeedPortfolio(newbase) + value := portfolio.GetPortfolioByExchange("ANX") + result, ok := value["LTC"] + if !ok { + t.Error("Test Failed - portfolio_test.go - GetPortfolioByExchange error") + } + + if result != 0.07 { + t.Error("Test Failed - portfolio_test.go - GetPortfolioByExchange result != 0.10") + } + + value = portfolio.GetPortfolioByExchange("Bitfinex") + result, ok = value["LTC"] + if !ok { + t.Error("Test Failed - portfolio_test.go - GetPortfolioByExchange error") + } + + if result != 0.05 { + t.Error("Test Failed - portfolio_test.go - GetPortfolioByExchange result != 0.05") + } } func TestGetExchangePortfolio(t *testing.T) { newbase := Base{} - newbase.AddAddress("someaddress", "LTC", "LTCWALLETTEST", 0.02) + newbase.AddAddress("ANX", "LTC", PortfolioAddressExchange, 0.03) + newbase.AddAddress("Bitfinex", "LTC", PortfolioAddressExchange, 0.05) + newbase.AddAddress("someaddress", "LTC", PortfolioAddressPersonal, 0.03) portfolio := GetPortfolio() portfolio.SeedPortfolio(newbase) value := portfolio.GetExchangePortfolio() - _, ok := value["ANX"] - if ok { + + result, ok := value["LTC"] + if !ok { t.Error("Test Failed - portfolio_test.go - GetExchangePortfolio error") } + + if result != 0.08 { + t.Error("Test Failed - portfolio_test.go - GetExchangePortfolio result != 0.08") + } } func TestGetPersonalPortfolio(t *testing.T) { newbase := Base{} newbase.AddAddress("someaddress", "LTC", "LTCWALLETTEST", 0.02) + newbase.AddAddress("anotheraddress", "LTC", "LTCWALLETTEST", 0.03) + newbase.AddAddress("Exchange", "LTC", PortfolioAddressExchange, 0.01) portfolio := GetPortfolio() portfolio.SeedPortfolio(newbase) value := portfolio.GetPersonalPortfolio() - _, ok := value["LTC"] + result, ok := value["LTC"] if !ok { t.Error("Test Failed - portfolio_test.go - GetPersonalPortfolio error") } + + if result != 0.05 { + t.Error("Test Failed - portfolio_test.go - GetPersonalPortfolio result != 0.05") + } } func TestGetPortfolioSummary(t *testing.T) { newbase := Base{} - newbase.AddAddress("someaddress", "LTC", "LTCWALLETTEST", 0.02) + // Personal holdings + newbase.AddAddress("someaddress", "LTC", PortfolioAddressPersonal, 1) + 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) + portfolio := GetPortfolio() portfolio.SeedPortfolio(newbase) - value := portfolio.GetPortfolioSummary("LTC") - if value["LTC"] != 0.02 { - t.Error("Test Failed - portfolio_test.go - GetPortfolioGroupedCoin error") + value := portfolio.GetPortfolioSummary() + + getTotalsVal := func(s string) Coin { + for x := range value.Totals { + if value.Totals[x].Coin == s { + return value.Totals[x] + } + } + return Coin{} + } + + if getTotalsVal("LTC").Coin != "LTC" { + t.Error("Test Failed - portfolio_test.go - TestGetPortfolioSummary error") + } + + if getTotalsVal("ETH").Coin != "ETH" { + t.Error("Test Failed - portfolio_test.go - TestGetPortfolioSummary error") + } + + if getTotalsVal("LTC").Balance != 101 { + t.Error("Test Failed - portfolio_test.go - TestGetPortfolioSummary error") } } func TestGetPortfolioGroupedCoin(t *testing.T) { newbase := Base{} newbase.AddAddress("someaddress", "LTC", "LTCWALLETTEST", 0.02) + newbase.AddAddress("Exchange", "LTC", PortfolioAddressExchange, 0.05) portfolio := GetPortfolio() portfolio.SeedPortfolio(newbase) value := portfolio.GetPortfolioGroupedCoin() - if value["LTC"][0] != "someaddress" { + if value["LTC"][0] != "someaddress" && len(value["LTC"][0]) != 1 { t.Error("Test Failed - portfolio_test.go - GetPortfolioGroupedCoin error") } } diff --git a/portfolio_routes.go b/portfolio_routes.go new file mode 100644 index 00000000..d2117160 --- /dev/null +++ b/portfolio_routes.go @@ -0,0 +1,27 @@ +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_router.go b/restful_router.go index bc97ff40..350ba9c9 100644 --- a/restful_router.go +++ b/restful_router.go @@ -13,6 +13,7 @@ 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 { var handler http.Handler diff --git a/tools/portfolio/portfolio.go b/tools/portfolio/portfolio.go index 3df8cf1a..a7b806fb 100644 --- a/tools/portfolio/portfolio.go +++ b/tools/portfolio/portfolio.go @@ -2,6 +2,7 @@ package main import ( "flag" + "fmt" "log" "net/url" @@ -12,89 +13,152 @@ import ( "github.com/thrasher-/gocryptotrader/portfolio" ) +var ( + priceMap map[string]float64 +) + +func printSummary(msg, from, to 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)) + } + log.Println() +} + +func getOnlineOfflinePortfolio(coins []portfolio.Coin, online bool) { + var totals float64 + for _, x := range coins { + value := priceMap[x.Coin] * x.Balance + totals += value + log.Printf("\t%v %v Subtotal: $%.2f Coin percentage: %.2f%%\n", x.Coin, + x.Balance, value, x.Percentage) + } + if !online { + printSummary("\tOffline balance", "USD", "AUD", totals) + } else { + printSummary("\tOnline balance", "USD", "AUD", totals) + } +} + func main() { var inFile, key string - var err error flag.StringVar(&inFile, "infile", "config.dat", "The config input file to process.") flag.StringVar(&key, "key", "", "The key to use for AES encryption.") flag.Parse() log.Println("GoCryptoTrader: portfolio tool.") - var data []byte var cfg config.Config - - data, err = common.ReadFile(inFile) + var err = cfg.ReadConfig(inFile) if err != nil { - log.Fatalf("Unable to read input file %s. Error: %s.", inFile, err) - } - - if config.ConfirmECS(data) { - if key == "" { - result, errf := config.PromptForConfigKey() - if errf != nil { - log.Fatal("Unable to obtain encryption/decryption key.") - } - key = string(result) - } - data, err = config.DecryptConfigFile(data, []byte(key)) - if err != nil { - log.Fatalf("Unable to decrypt config data. Error: %s.", err) - } - - } - err = config.ConfirmConfigJSON(data, &cfg) - if err != nil { - log.Fatal("File isn't in JSON format") + log.Fatal(err) } + log.Println("Loaded config file.") port := portfolio.Base{} port.SeedPortfolio(cfg.Portfolio) - result := port.GetPortfolioSummary("") + result := port.GetPortfolioSummary() + + log.Println("Fetched portfolio data.") type PortfolioTemp struct { Balance float64 Subtotal float64 } - stuff := make(map[string]PortfolioTemp) + cfg.RetrieveConfigCurrencyPairs() + portfolioMap := make(map[string]PortfolioTemp) total := float64(0) - for x, y := range result { - if x == "ETH" { - y = y / common.WeiPerEther + log.Println("Fetching currency data..") + var fiatCurrencies []string + for _, y := range result.Totals { + if currency.IsFiatCurrency(y.Coin) { + fiatCurrencies = append(fiatCurrencies, y.Coin) } + } + err = currency.SeedCurrencyData(common.JoinStrings(fiatCurrencies, ",")) + if err != nil { + log.Fatal(err) + } + log.Println("Fetched currency data.") + log.Println("Fetching ticker data and calculating totals..") + priceMap = make(map[string]float64) + priceMap["USD"] = 1 + + for _, y := range result.Totals { pf := PortfolioTemp{} - pf.Balance = y + pf.Balance = y.Balance pf.Subtotal = 0 - bf := bitfinex.Bitfinex{} - - if currency.IsDefaultCurrency(x) { - continue - } - - ticker, errf := bf.GetTicker(x+"USD", url.Values{}) - if errf != nil { - log.Println(errf) + if currency.IsDefaultCurrency(y.Coin) { + if y.Coin != "USD" { + conv, err := currency.ConvertCurrency(y.Balance, y.Coin, "USD") + if err != nil { + log.Println(err) + } else { + priceMap[y.Coin] = conv / y.Balance + pf.Subtotal = conv + } + } else { + pf.Subtotal = y.Balance + } } else { - pf.Subtotal = ticker.Last * y + bf := bitfinex.Bitfinex{} + ticker, errf := bf.GetTicker(y.Coin+"USD", url.Values{}) + if errf != nil { + log.Println(errf) + } else { + priceMap[y.Coin] = ticker.Last + pf.Subtotal = ticker.Last * y.Balance + } } - stuff[x] = pf + portfolioMap[y.Coin] = pf total += pf.Subtotal } + log.Println("Done.") + log.Println() + log.Println("PORTFOLIO TOTALS:") + 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) - for x, y := range stuff { - log.Printf("%s %f subtotal: %f USD (1 %s = %.2f USD). Percentage of portfolio %f", x, y.Balance, y.Subtotal, x, y.Subtotal/y.Balance, y.Subtotal/total*100/1) + log.Println("OFFLINE COIN TOTALS:") + getOnlineOfflinePortfolio(result.Offline, false) + + log.Println("ONLINE COIN TOTALS:") + getOnlineOfflinePortfolio(result.Online, true) + + log.Println("OFFLINE COIN SUMMARY:") + var totals float64 + for x, y := range result.OfflineSummary { + log.Printf("\t%s:", x) + totals = 0 + for z := range y { + value := priceMap[x] * y[z].Balance + totals += value + 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) } - log.Printf("Total balance in USD: %f.\n", total) - - conv, err := currency.ConvertCurrency(total, "USD", "AUD") - if err != nil { - log.Println(err) - } else { - log.Printf("Total balance in AUD: %f.\n", conv) + log.Println("ONLINE COINS SUMMARY:") + for x, y := range result.OnlineSummary { + log.Printf("\t%s:", x) + totals = 0 + for z, w := range y { + value := priceMap[z] * w.Balance + totals += value + log.Printf("\t %s Amount: %f Subtotal $%.2f Coin percentage: %.2f%%", + z, w.Balance, value, w.Percentage) + } + printSummary("\t Exchange balance", "USD", "AUD", totals) } }