Merge branch 'lyricnz-feature/portfolio-buy2'

pull/259/head
Miguel Mota 3 years ago
commit b921c091d6
No known key found for this signature in database
GPG Key ID: 67EC1161588A00F9

@ -69,6 +69,9 @@ func ActionsMap() map[string]bool {
"move_down_or_next_page": true,
"show_price_alert_add_menu": true,
"sort_column_balance": true,
"sort_column_cost": true,
"sort_column_pnl": true,
"sort_column_pnl_percent": true,
}
}

@ -25,6 +25,8 @@ type Coin struct {
// for portfolio
Holdings float64
Balance float64
BuyPrice float64
BuyCurrency string
}
// AllCoins returns a slice of all the coins

@ -92,6 +92,7 @@ type State struct {
tableCompactNotation bool
favoritesCompactNotation bool
portfolioCompactNotation bool
enableMouse bool
}
// Cointop cointop
@ -127,6 +128,8 @@ type Cointop struct {
type PortfolioEntry struct {
Coin string
Holdings float64
BuyPrice float64
BuyCurrency string
}
// Portfolio is portfolio structure
@ -187,6 +190,9 @@ var DefaultChartRange = "1Y"
// DefaultCompactNotation ...
var DefaultCompactNotation = false
// DefaultEnableMouse ...
var DefaultEnableMouse = true
// DefaultMaxChartWidth ...
var DefaultMaxChartWidth = 175
@ -296,6 +302,7 @@ func NewCointop(config *Config) (*Cointop, error) {
SoundEnabled: true,
},
compactNotation: DefaultCompactNotation,
enableMouse: DefaultEnableMouse,
tableCompactNotation: DefaultCompactNotation,
favoritesCompactNotation: DefaultCompactNotation,
portfolioCompactNotation: DefaultCompactNotation,
@ -488,7 +495,7 @@ func (ct *Cointop) Run() error {
defer ui.Close()
ui.SetInputEsc(true)
ui.SetMouse(true)
ui.SetMouse(ct.State.enableMouse)
ui.SetHighlight(true)
ui.SetManagerFunc(ct.layout)
if err := ct.SetKeybindings(); err != nil {

@ -49,6 +49,7 @@ type ConfigFileConfig struct {
RefreshRate interface{} `toml:"refresh_rate"`
CacheDir interface{} `toml:"cache_dir"`
CompactNotation interface{} `toml:"compact_notation"`
EnableMouse interface{} `toml:"enable_mouse"`
Table map[string]interface{} `toml:"table"`
Chart map[string]interface{} `toml:"chart"`
}
@ -72,6 +73,7 @@ func (ct *Cointop) SetupConfig() error {
ct.loadRefreshRateFromConfig,
ct.loadCacheDirFromConfig,
ct.loadCompactNotationFromConfig,
ct.loadEnableMouseFromConfig,
ct.loadPriceAlertsFromConfig,
ct.loadPortfolioFromConfig,
}
@ -227,9 +229,12 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
if !ok || entry.Coin == "" {
continue
}
amount := strconv.FormatFloat(entry.Holdings, 'f', -1, 64)
coinName := entry.Coin
tuple := []string{coinName, amount}
tuple := []string{
entry.Coin,
strconv.FormatFloat(entry.Holdings, 'f', -1, 64),
strconv.FormatFloat(entry.BuyPrice, 'f', -1, 64),
entry.BuyCurrency,
}
holdingsIfc = append(holdingsIfc, tuple)
}
sort.Slice(holdingsIfc, func(i, j int) bool {
@ -289,6 +294,7 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
Table: tableMapIfc,
Chart: chartMapIfc,
CompactNotation: ct.State.compactNotation,
EnableMouse: ct.State.enableMouse,
}
var b bytes.Buffer
@ -506,6 +512,16 @@ func (ct *Cointop) loadCompactNotationFromConfig() error {
return nil
}
// loadCompactNotationFromConfig loads compact-notation setting from config file to struct
func (ct *Cointop) loadEnableMouseFromConfig() error {
log.Debug("loadEnableMouseFromConfig()")
if enableMouse, ok := ct.config.EnableMouse.(bool); ok {
ct.State.enableMouse = enableMouse
}
return nil
}
// LoadAPIChoiceFromConfig loads API choices from config file to struct
func (ct *Cointop) loadAPIChoiceFromConfig() error {
log.Debug("loadAPIKeysFromConfig()")
@ -584,48 +600,72 @@ func (ct *Cointop) loadPortfolioFromConfig() error {
}
}
} else if key == "holdings" {
holdingsIfc, ok := valueIfc.([]interface{})
if !ok {
continue
// Defer until the end to work around premature-save issue
} else if key == "compact_notation" {
ct.State.portfolioCompactNotation = valueIfc.(bool)
} else {
// Backward compatibility < v1.6.0
holdings, err := ct.InterfaceToFloat64(valueIfc)
if err != nil {
return err
}
if err := ct.SetPortfolioEntry(key, holdings, 0.0, ""); err != nil {
return err
}
}
}
// Process holdings last because it causes a ct.Save()
if valueIfc, ok := ct.config.Portfolio["holdings"]; ok {
if holdingsIfc, ok := valueIfc.([]interface{}); ok {
ct.loadPortfolioHoldingsFromConfig(holdingsIfc)
}
}
return nil
}
func (ct *Cointop) loadPortfolioHoldingsFromConfig(holdingsIfc []interface{}) error {
for _, itemIfc := range holdingsIfc {
tupleIfc, ok := itemIfc.([]interface{})
if !ok {
continue
}
if len(tupleIfc) > 2 {
if len(tupleIfc) > 4 {
continue
}
name, ok := tupleIfc[0].(string)
if !ok {
continue
continue // was not a string
}
holdings, err := ct.InterfaceToFloat64(tupleIfc[1])
if err != nil {
return nil
return err // was not a float64
}
if err := ct.SetPortfolioEntry(name, holdings); err != nil {
buyPrice := 0.0
if len(tupleIfc) >= 3 {
if buyPrice, err = ct.InterfaceToFloat64(tupleIfc[2]); err != nil {
return err
}
}
} else if key == "compact_notation" {
ct.State.portfolioCompactNotation = valueIfc.(bool)
buyCurrency := ""
if len(tupleIfc) >= 4 {
if parseCurrency, ok := tupleIfc[3].(string); !ok {
return err // was not a string
} else {
// Backward compatibility < v1.6.0
holdings, err := ct.InterfaceToFloat64(valueIfc)
if err != nil {
return err
buyCurrency = parseCurrency
}
}
if err := ct.SetPortfolioEntry(key, holdings); err != nil {
// Watch out - this calls ct.Save() which may save a half-loaded configuration
if err := ct.SetPortfolioEntry(name, holdings, buyPrice, buyCurrency); err != nil {
return err
}
}
}
return nil
}

@ -12,7 +12,7 @@ import (
log "github.com/sirupsen/logrus"
)
// FiatCurrencyNames is a mpa of currency symbols to names.
// FiatCurrencyNames is a map of currency symbols to names.
// Keep these in alphabetical order.
var FiatCurrencyNames = map[string]string{
"AUD": "Australian Dollar",
@ -301,3 +301,20 @@ func CurrencySymbol(currency string) string {
return "?"
}
// Convert converts an amount to another currency type
func (ct *Cointop) Convert(convertFrom, convertTo string, amount float64) (float64, error) {
convertFrom = strings.ToLower(convertFrom)
convertTo = strings.ToLower(convertTo)
if convertFrom == convertTo {
return amount, nil
}
rate, err := ct.api.GetExchangeRate(convertFrom, convertTo, true)
if err != nil {
return 0, err
}
return rate * amount, nil
}

@ -85,5 +85,8 @@ func DefaultShortcuts() map[string]string {
"<": "scroll_left",
"+": "show_price_alert_add_menu",
"\\\\": "toggle_table_fullscreen",
"!": "sort_column_cost",
"@": "sort_column_pnl",
"#": "sort_column_pnl_percent",
}
}

@ -325,6 +325,12 @@ func (ct *Cointop) SetKeybindingAction(shortcutKey string, action string) error
fn = ct.Keyfn(ct.CursorDownOrNextPage)
case "move_up_or_previous_page":
fn = ct.Keyfn(ct.CursorUpOrPreviousPage)
case "sort_column_cost":
fn = ct.Sortfn("cost", true)
case "sort_column_pnl":
fn = ct.Sortfn("pnl", true)
case "sort_column_pnl_percent":
fn = ct.Sortfn("pnl_percent", true)
default:
fn = ct.Keyfn(ct.Noop)
}

@ -35,6 +35,10 @@ var SupportedPortfolioTableHeaders = []string{
"1y_change",
"percent_holdings",
"last_updated",
"cost_price",
"cost",
"pnl",
"pnl_percent",
}
// DefaultPortfolioTableHeaders are the default portfolio table header columns
@ -49,12 +53,23 @@ var DefaultPortfolioTableHeaders = []string{
"24h_change",
"7d_change",
"percent_holdings",
"cost_price",
"cost",
"pnl",
"pnl_percent",
"last_updated",
}
// HiddenBalanceChars are the characters to show when hidding balances
var HiddenBalanceChars = "********"
var costColumns = map[string]bool{
"cost_price": true,
"cost": true,
"pnl": true,
"pnl_percent": true,
}
// ValidPortfolioTableHeader returns the portfolio table headers
func (ct *Cointop) ValidPortfolioTableHeader(name string) bool {
for _, v := range SupportedPortfolioTableHeaders {
@ -80,6 +95,25 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
headers := ct.GetPortfolioTableHeaders()
ct.ClearSyncMap(&ct.State.tableColumnWidths)
ct.ClearSyncMap(&ct.State.tableColumnAlignLeft)
displayCostColumns := false
for _, coin := range ct.State.coins {
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
displayCostColumns = true
break
}
}
if !displayCostColumns {
filtered := make([]string, 0)
for _, header := range headers {
if _, ok := costColumns[header]; !ok {
filtered = append(filtered, header)
}
}
headers = filtered
}
for _, coin := range ct.State.coins {
leftMargin := 1
rightMargin := 1
@ -301,6 +335,117 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
Color: ct.colorscheme.TableRow,
Text: lastUpdated,
})
case "cost_price":
text := fmt.Sprintf("%s %s", coin.BuyCurrency, ct.FormatPrice(coin.BuyPrice))
if coin.BuyPrice == 0.0 || coin.BuyCurrency == "" {
text = ""
}
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
}
symbolPadding := 1
ct.SetTableColumnWidth(header, utf8.RuneCountInString(text)+symbolPadding)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: ct.colorscheme.TableRow,
Text: text,
})
case "cost":
cost := 0.0
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
costPrice, err := ct.Convert(coin.BuyCurrency, ct.State.currencyConversion, coin.BuyPrice)
if err == nil {
cost = costPrice * coin.Holdings
}
}
text := humanize.FixedMonetaryf(cost, 2)
if coin.BuyPrice == 0.0 {
text = ""
}
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
}
symbolPadding := 1
ct.SetTableColumnWidth(header, utf8.RuneCountInString(text)+symbolPadding)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: ct.colorscheme.TableColumnPrice,
Text: text,
})
case "pnl":
text := ""
colorProfit := ct.colorscheme.TableColumnChange
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
costPrice, err := ct.Convert(coin.BuyCurrency, ct.State.currencyConversion, coin.BuyPrice)
if err == nil {
profit := (coin.Price - costPrice) * coin.Holdings
text = humanize.FixedMonetaryf(profit, 2)
if profit > 0 {
colorProfit = ct.colorscheme.TableColumnChangeUp
} else if profit < 0 {
colorProfit = ct.colorscheme.TableColumnChangeDown
}
} else {
text = "?"
}
}
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
colorProfit = ct.colorscheme.TableColumnChange
}
symbolPadding := 1
ct.SetTableColumnWidth(header, utf8.RuneCountInString(text)+symbolPadding)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: colorProfit,
Text: text,
})
case "pnl_percent":
profitPercent := 0.0
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
costPrice, err := ct.Convert(coin.BuyCurrency, ct.State.currencyConversion, coin.BuyPrice)
if err == nil {
profitPercent = 100 * (coin.Price/costPrice - 1)
}
}
colorProfit := ct.colorscheme.TableColumnChange
if profitPercent > 0 {
colorProfit = ct.colorscheme.TableColumnChangeUp
} else if profitPercent < 0 {
colorProfit = ct.colorscheme.TableColumnChangeDown
}
text := fmt.Sprintf("%.2f%%", profitPercent)
if coin.BuyPrice == 0.0 {
text = ""
}
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
colorProfit = ct.colorscheme.TableColumnChange
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: colorProfit,
Text: text,
})
}
}
@ -456,8 +601,12 @@ func (ct *Cointop) SetPortfolioHoldings() error {
}
shouldDelete := holdings == 0
// TODO: add fields to form, parse here
buyPrice := 0.0
buyCurrency := ""
idx := ct.GetPortfolioCoinIndex(coin)
if err := ct.SetPortfolioEntry(coin.Name, holdings); err != nil {
if err := ct.SetPortfolioEntry(coin.Name, holdings, buyPrice, buyCurrency); err != nil {
return err
}
@ -503,7 +652,7 @@ func (ct *Cointop) PortfolioEntry(c *Coin) (*PortfolioEntry, bool) {
}
// SetPortfolioEntry sets a portfolio entry
func (ct *Cointop) SetPortfolioEntry(coin string, holdings float64) error {
func (ct *Cointop) SetPortfolioEntry(coin string, holdings float64, buyPrice float64, buyCurrency string) error {
log.Debug("SetPortfolioEntry()")
ic, _ := ct.State.allCoinsSlugMap.Load(strings.ToLower(coin))
c, _ := ic.(*Coin)
@ -513,6 +662,8 @@ func (ct *Cointop) SetPortfolioEntry(coin string, holdings float64) error {
ct.State.portfolio.Entries[key] = &PortfolioEntry{
Coin: coin,
Holdings: holdings,
BuyPrice: buyPrice,
BuyCurrency: buyCurrency,
}
} else {
p.Holdings = holdings
@ -564,6 +715,8 @@ func (ct *Cointop) GetPortfolioSlice() []*Coin {
continue
}
coin.Holdings = p.Holdings
coin.BuyPrice = p.BuyPrice
coin.BuyCurrency = p.BuyCurrency
balance := coin.Price * p.Holdings
balancestr := fmt.Sprintf("%.2f", balance)
if ct.State.currencyConversion == "ETH" || ct.State.currencyConversion == "BTC" {
@ -688,7 +841,7 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
records := make([][]string, len(holdings))
symbol := ct.CurrencySymbol()
headers := []string{"name", "symbol", "price", "holdings", "balance", "24h%", "%holdings"}
headers := []string{"name", "symbol", "price", "holdings", "balance", "24h%", "%holdings", "cost_price", "cost", "pnl", "pnl_percent"}
if len(filterCols) > 0 {
for _, col := range filterCols {
valid := false
@ -785,6 +938,70 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
if hideBalances {
item[i] = HiddenBalanceChars
}
case "cost_price":
if entry.BuyPrice > 0 && entry.BuyCurrency != "" {
if humanReadable {
item[i] = fmt.Sprintf("%s %s", entry.BuyCurrency, ct.FormatPrice(entry.BuyPrice))
} else {
item[i] = fmt.Sprintf("%s %s", entry.BuyCurrency, strconv.FormatFloat(entry.BuyPrice, 'f', -1, 64))
}
}
if hideBalances {
item[i] = HiddenBalanceChars
}
case "cost":
if entry.BuyPrice > 0 && entry.BuyCurrency != "" {
costPrice, err := ct.Convert(entry.BuyCurrency, ct.State.currencyConversion, entry.BuyPrice)
if err == nil {
cost := costPrice * entry.Holdings
if humanReadable {
item[i] = fmt.Sprintf("%s%s", symbol, humanize.FixedMonetaryf(cost, 2))
} else {
item[i] = strconv.FormatFloat(cost, 'f', -1, 64)
}
} else {
item[i] = "?" // error
}
}
if hideBalances {
item[i] = HiddenBalanceChars
}
case "pnl":
if entry.BuyPrice > 0 && entry.BuyCurrency != "" {
costPrice, err := ct.Convert(entry.BuyCurrency, ct.State.currencyConversion, entry.BuyPrice)
if err == nil {
profit := (entry.Price - costPrice) * entry.Holdings
if humanReadable {
// TODO: if <0 "£-3.71" should be "-£3.71"?
item[i] = fmt.Sprintf("%s%s", symbol, humanize.FixedMonetaryf(profit, 2))
} else {
item[i] = strconv.FormatFloat(profit, 'f', -1, 64)
}
} else {
item[i] = "?" // error
}
}
if hideBalances {
item[i] = HiddenBalanceChars
}
case "pnl_percent":
if entry.BuyPrice > 0 && entry.BuyCurrency != "" {
costPrice, err := ct.Convert(entry.BuyCurrency, ct.State.currencyConversion, entry.BuyPrice)
if err == nil {
profitPercent := 100 * (entry.Price/costPrice - 1)
if humanReadable {
item[i] = fmt.Sprintf("%s%%", humanize.Numericf(profitPercent, 2))
} else {
item[i] = fmt.Sprintf("%.2f", profitPercent)
}
} else {
item[i] = "?" // error
}
}
if hideBalances {
item[i] = HiddenBalanceChars
}
}
}
records[i] = item

@ -68,6 +68,14 @@ func (ct *Cointop) Sort(sortBy string, desc bool, list []*Coin, renderHeaders bo
return a.AvailableSupply < b.AvailableSupply
case "last_updated":
return a.LastUpdated < b.LastUpdated
case "cost_price":
return a.BuyPrice < b.BuyPrice
case "cost":
return (a.BuyPrice * a.Holdings) < (b.BuyPrice * b.Holdings) // TODO: convert?
case "pnl":
return (a.Price - a.BuyPrice) < (b.Price - b.BuyPrice)
case "pnl_percent":
return (a.Price - a.BuyPrice) < (b.Price - b.BuyPrice)
default:
return a.Rank < b.Rank
}

@ -126,6 +126,26 @@ var HeaderColumns = map[string]*HeaderColumn{
Label: "last [u]pdated",
PlainLabel: "last updated",
},
"cost_price": {
Slug: "cost_price",
Label: "cost price",
PlainLabel: "cost price",
},
"cost": {
Slug: "cost",
Label: "[!]cost",
PlainLabel: "cost",
},
"pnl": {
Slug: "pnl",
Label: "[@]PNL",
PlainLabel: "PNL",
},
"pnl_percent": {
Slug: "pnl_percent",
Label: "[#]PNL%",
PlainLabel: "PNL%",
},
}
// GetLabel fetch the label to use for the heading (depends on configuration)
@ -211,7 +231,7 @@ func (ct *Cointop) UpdateTableHeader() error {
}
leftAlign := ct.GetTableColumnAlignLeft(col)
switch col {
case "price", "balance":
case "price", "balance", "pnl", "cost":
label = fmt.Sprintf("%s%s", ct.CurrencySymbol(), label)
}
if leftAlign {
@ -265,6 +285,9 @@ func (ct *Cointop) SetTableColumnWidth(header string, width int) {
prev = prevIfc.(int)
} else {
hc := HeaderColumns[header]
if hc == nil {
log.Warnf("SetTableColumnWidth(%s) not found", header)
}
prev = utf8.RuneCountInString(ct.GetLabel(hc)) + 1
switch header {
case "price", "balance":

@ -184,6 +184,29 @@ draft: false
Your portfolio is autosaved after you edit holdings. You can also press <kbd>ctrl</kbd>+<kbd>s</kbd> to manually save your portfolio holdings to the config file.
## How do I include buy/cost price in my portfolio?
Currently there is no UI for this. If you want to include the cost of your coins in the Portfolio screen, you will need to edit your config.toml
Each coin consists of four values: coin name, coin amount, cost-price, cost-currency.
For example, the following configuration includes 100 ALGO at USD1.95 each; and 0.1 BTC at AUD50100.83 each.
```toml
holdings = [["Algorand", "100", "1.95", "USD"], ["Bitcoin", "0.1", "50100.83", "AUD"]]
```
With this configuration, four new columns are useful:
- `cost_price` the price and currency that the coins were purchased at
- `cost` the cost (in the current currency) of the coins
- `pnl` the PNL of the coins (current value vs original cost)
- `pnl_percent` the PNL of the coins as a fraction of the original cost
With the holdings above, and the currency set to GBP (British Pounds) cointop will look something like this:
![portfolio profit and loss](https://user-images.githubusercontent.com/122371/138361142-8e1f32b5-ca24-471d-a628-06968f07c65f.png)
## How do I hide my portfolio balances (private mode)?
You can run cointop with the `--hide-portfolio-balances` flag to hide portfolio balances or use the keyboard shortcut <kbd>Ctrl</kbd>+<kbd>space</kbd> on the portfolio page to toggle hide/show.

@ -12,6 +12,7 @@ import (
apitypes "github.com/cointop-sh/cointop/pkg/api/types"
"github.com/cointop-sh/cointop/pkg/api/util"
gecko "github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/v3"
"github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/v3/types"
geckoTypes "github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/v3/types"
)
@ -33,6 +34,7 @@ type Service struct {
maxResultsPerPage uint
maxPages uint
cacheMap sync.Map
cachedRates *types.ExchangeRatesItem
}
// NewCoinGecko new service
@ -146,6 +148,45 @@ func (s *Service) GetCoinGraphData(convert, symbol, name string, start, end int6
return ret, nil
}
// GetCachedExchangeRates returns an indefinitely cached set of exchange rates
func (s *Service) GetExchangeRates(cached bool) (*types.ExchangeRatesItem, error) {
if s.cachedRates == nil || !cached {
rates, err := s.client.ExchangeRates()
if err != nil {
return nil, err
}
s.cachedRates = rates
}
return s.cachedRates, nil
}
// GetExchangeRate gets the current excange rate between two currencies
func (s *Service) GetExchangeRate(convertFrom, convertTo string, cached bool) (float64, error) {
convertFrom = strings.ToLower(convertFrom)
convertTo = strings.ToLower(convertTo)
if convertFrom == convertTo {
return 1.0, nil
}
rates, err := s.GetExchangeRates(cached)
if err != nil {
return 0, err
}
if rates == nil {
return 0, fmt.Errorf("expected rates, received nil")
}
// Combined rate is convertFrom->BTC->convertTo
fromRate, found := (*rates)[convertFrom]
if !found {
return 0, fmt.Errorf("unsupported currency conversion: %s", convertFrom)
}
toRate, found := (*rates)[convertTo]
if !found {
return 0, fmt.Errorf("unsupported currency conversion: %s", convertTo)
}
rate := toRate.Value / fromRate.Value
return rate, nil
}
// GetGlobalMarketGraphData gets global market graph data
func (s *Service) GetGlobalMarketGraphData(convert string, start int64, end int64) (apitypes.MarketGraph, error) {
days := strconv.Itoa(util.CalcDays(start, end))
@ -160,26 +201,11 @@ func (s *Service) GetGlobalMarketGraphData(convert string, start int64, end int6
}
// This API does not appear to support vs_currency and only returns USD, so use ExchangeRates to convert
rate := 1.0
if convertTo != "usd" {
rates, err := s.client.ExchangeRates()
// TODO: watch out - this is not cached, so we hit the backend every time!
rate, err := s.GetExchangeRate("usd", convertTo, true)
if err != nil {
return ret, err
}
if rates == nil {
return ret, fmt.Errorf("expected rates, received nil")
}
// Combined rate is USD->BTC->other
btcRate, found := (*rates)[convertTo]
if !found {
return ret, fmt.Errorf("unsupported currency conversion: %s", convertTo)
}
usdRate, found := (*rates)["usd"]
if !found {
return ret, fmt.Errorf("unsupported currency conversion: usd")
}
rate = btcRate.Value / usdRate.Value
}
var marketCapUSD [][]float64
var marketVolumeUSD [][]float64

@ -430,3 +430,11 @@ func getChartInterval(start, end int64) string {
}
return interval
}
// GetExchangeRate gets the current excange rate between two currencies
func (s *Service) GetExchangeRate(convertFrom, convertTo string, cached bool) (float64, error) {
if convertFrom == convertTo {
return 1.0, nil
}
return 0, fmt.Errorf("unsupported currency conversion: %s => %s", convertFrom, convertTo)
}

@ -16,4 +16,5 @@ type Interface interface {
CoinLink(name string) string
SupportedCurrencies() []string
Price(name string, convert string) (float64, error)
GetExchangeRate(convertFrom, convertTo string, cached bool) (float64, error) // I don't love this caching
}

@ -34,6 +34,11 @@ func Monetaryf(value float64, precision int) string {
return f(value, precision, "LC_MONETARY", false)
}
// FixedMonetaryf produces a fixed-precision monetary-value string. See Monetaryf.
func FixedMonetaryf(value float64, precision int) string {
return f(value, precision, "LC_MONETARY", true)
}
// borrowed from go-locale/util.go
func splitLocale(locale string) (string, string) {
// Remove the encoding, if present

@ -38,12 +38,12 @@ func (ui *UI) SetBgColor(bgColor gocui.Attribute) {
// SetInputEsc enables the escape key
func (ui *UI) SetInputEsc(enabled bool) {
ui.g.InputEsc = true
ui.g.InputEsc = enabled
}
// SetMouse enables the mouse
func (ui *UI) SetMouse(enabled bool) {
ui.g.Mouse = true
ui.g.Mouse = enabled
}
// SetCursor enables the input field cursor
@ -53,7 +53,7 @@ func (ui *UI) SetCursor(enabled bool) {
// SetHighlight enables the highlight active state
func (ui *UI) SetHighlight(enabled bool) {
ui.g.Highlight = true
ui.g.Highlight = enabled
}
// SetManagerFunc sets the function to call for rendering UI

Loading…
Cancel
Save