From c36f3de781aa5b75e6963507d49cde01cb2c3d7d Mon Sep 17 00:00:00 2001 From: Miguel Mota Date: Fri, 29 Jan 2021 23:22:17 -0800 Subject: [PATCH] Add configurable portfolio table columns --- cointop/config.go | 106 +++++++++++++++-------- cointop/portfolio.go | 187 ++++++++++++++++++++++++++-------------- cointop/price_alerts.go | 8 +- 3 files changed, 198 insertions(+), 103 deletions(-) diff --git a/cointop/config.go b/cointop/config.go index e2f9e50b..9825a26d 100644 --- a/cointop/config.go +++ b/cointop/config.go @@ -58,9 +58,6 @@ func (ct *Cointop) SetupConfig() error { if err := ct.loadTableColumnsFromConfig(); err != nil { return err } - if err := ct.loadPortfolioColumnsFromConfig(); err != nil { - return err - } if err := ct.loadShortcutsFromConfig(); err != nil { return err } @@ -241,6 +238,9 @@ func (ct *Cointop) configToToml() ([]byte, error) { var tuple []string = []string{coinName, amount} holdingsIfc = append(holdingsIfc, tuple) } + sort.Slice(holdingsIfc, func(i, j int) bool { + return holdingsIfc[i][0] < holdingsIfc[j][0] + }) portfolioIfc["holdings"] = holdingsIfc if reflect.DeepEqual(DefaultPortfolioTableHeaders, ct.State.portfolioTableColumns) { @@ -338,11 +338,6 @@ func (ct *Cointop) loadTableColumnsFromConfig() error { return nil } -// LoadPortfolioColumnsFromConfig loads preferred portfolio table columns from config file to struct -func (ct *Cointop) loadPortfolioColumnsFromConfig() error { - return nil -} - // LoadShortcutsFromConfig loads keyboard shortcuts from config file to struct func (ct *Cointop) loadShortcutsFromConfig() error { ct.debuglog("loadShortcutsFromConfig()") @@ -508,41 +503,88 @@ func (ct *Cointop) loadPortfolioFromConfig() error { for key, valueIfc := range ct.config.Portfolio { if key == "columns" { - } - if key == "holdings" { - holdingsIfc, ok := valueIfc.([][]interface{}) - if !ok { - continue - } - fmt.Println(holdingsIfc) - } - } - /* - for name, holdingsIfc := range ct.config.Portfolio { - if name == "columns" { - continue + var columns []string + ifcs, ok := valueIfc.([]interface{}) + if ok { + for _, ifc := range ifcs { + if v, ok := ifc.(string); ok { + if !ct.ValidPortfolioTableHeader(v) { + return fmt.Errorf("Invalid table header name %q. Valid names are: %s", v, strings.Join(DefaultPortfolioTableHeaders, ",")) + } + columns = append(columns, v) + } + } + if len(columns) > 0 { + ct.State.portfolioTableColumns = columns + } } - if name == "holdings" { + } else if key == "holdings" { + holdingsIfc, ok := valueIfc.([]interface{}) + if !ok { continue } - var holdings float64 - var ok bool - if holdings, ok = holdingsIfc.(float64); !ok { - if holdingsInt, ok := holdingsIfc.(int64); ok { - holdings = float64(holdingsInt) + for _, itemIfc := range holdingsIfc { + tupleIfc, ok := itemIfc.([]interface{}) + if !ok { + continue + } + if len(tupleIfc) > 2 { + continue + } + name, ok := tupleIfc[0].(string) + if !ok { + continue + } + + holdings, err := ct.InterfaceToFloat64(tupleIfc[1]) + if err != nil { + return nil + } + + if err := ct.SetPortfolioEntry(name, holdings); err != nil { + return err } } + } else { + // Backward compatibility < v1.6.0 + holdings, err := ct.InterfaceToFloat64(valueIfc) + if err != nil { + return err + } - if err := ct.SetPortfolioEntry(name, holdings); err != nil { + if err := ct.SetPortfolioEntry(key, holdings); err != nil { return err } } - */ + } return nil } +// InterfaceToFloat64 attempts to convert interface to float64 +func (ct *Cointop) InterfaceToFloat64(value interface{}) (float64, error) { + var num float64 + var err error + switch v := value.(type) { + case string: + num, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, err + } + case int: + num = float64(v) + case int32: + num = float64(v) + case int64: + num = float64(v) + case float64: + num = v + } + + return num, nil +} + // LoadPriceAlertsFromConfig loads price alerts from config file to struct func (ct *Cointop) loadPriceAlertsFromConfig() error { ct.debuglog("loadPriceAlertsFromConfig()") @@ -570,11 +612,7 @@ func (ct *Cointop) loadPriceAlertsFromConfig() error { if _, ok := PriceAlertOperatorMap[operator]; !ok { return ErrInvalidPriceAlert } - targetPriceStr, ok := priceAlert[2].(string) - if !ok { - return ErrInvalidPriceAlert - } - targetPrice, err := strconv.ParseFloat(targetPriceStr, 64) + targetPrice, err := ct.InterfaceToFloat64(priceAlert[2]) if err != nil { return err } diff --git a/cointop/portfolio.go b/cointop/portfolio.go index 1763ddf4..059a4b23 100644 --- a/cointop/portfolio.go +++ b/cointop/portfolio.go @@ -26,7 +26,9 @@ var DefaultPortfolioTableHeaders = []string{ "price", "holdings", "balance", + "1h_change", "24h_change", + "7d_change", "percent_holdings", "last_updated", } @@ -68,13 +70,27 @@ func (ct *Cointop) GetPortfolioTable() *table.Table { } colorbalance := ct.colorscheme.TableColumnPrice + color1h := ct.colorscheme.TableColumnChange color24h := ct.colorscheme.TableColumnChange + color7d := ct.colorscheme.TableColumnChange + if coin.PercentChange1H > 0 { + color1h = ct.colorscheme.TableColumnChangeUp + } + if coin.PercentChange1H < 0 { + color1h = ct.colorscheme.TableColumnChangeDown + } if coin.PercentChange24H > 0 { color24h = ct.colorscheme.TableColumnChangeUp } if coin.PercentChange24H < 0 { color24h = ct.colorscheme.TableColumnChangeDown } + if coin.PercentChange7D > 0 { + color7d = ct.colorscheme.TableColumnChangeUp + } + if coin.PercentChange7D < 0 { + color7d = ct.colorscheme.TableColumnChangeDown + } percentHoldings := (coin.Balance / total) * 1e2 if math.IsNaN(percentHoldings) { @@ -83,71 +99,112 @@ func (ct *Cointop) GetPortfolioTable() *table.Table { unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64) lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02") - t.AddRowCells( - &table.RowCell{ - LeftMargin: 0, - Width: 6, - LeftAlign: false, - Color: ct.colorscheme.Default, - Text: rank, - }, - &table.RowCell{ - LeftMargin: 1, - Width: 22, - LeftAlign: true, - Color: namecolor, - Text: name, - }, - &table.RowCell{ - LeftMargin: 1, - Width: 6, - LeftAlign: true, - Color: ct.colorscheme.TableRow, - Text: symbol, - }, - &table.RowCell{ - LeftMargin: 1, - Width: 14, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: humanize.Commaf(coin.Price), - }, - &table.RowCell{ - LeftMargin: 1, - Width: 16, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: strconv.FormatFloat(coin.Holdings, 'f', -1, 64), - }, - &table.RowCell{ - LeftMargin: 1, - Width: 16, - LeftAlign: false, - Color: colorbalance, - Text: humanize.Commaf(coin.Balance), - }, - &table.RowCell{ - LeftMargin: 1, - Width: 10, - LeftAlign: false, - Color: color24h, - Text: fmt.Sprintf("%.2f%%", coin.PercentChange24H), - }, - &table.RowCell{ - LeftMargin: 1, - Width: 14, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: fmt.Sprintf("%.2f%%", percentHoldings), - }, - &table.RowCell{ - LeftMargin: 1, - Width: 18, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: lastUpdated, - }, - ) + headers := ct.GetPortfolioTableHeaders() + var rowCells []*table.RowCell + for _, header := range headers { + switch header { + case "rank": + rowCells = append(rowCells, &table.RowCell{ + LeftMargin: 0, + Width: 6, + LeftAlign: false, + Color: ct.colorscheme.Default, + Text: rank, + }) + case "name": + rowCells = append(rowCells, + &table.RowCell{ + LeftMargin: 1, + Width: 22, + LeftAlign: true, + Color: namecolor, + Text: name, + }) + case "symbol": + rowCells = append(rowCells, + &table.RowCell{ + LeftMargin: 1, + Width: 6, + LeftAlign: true, + Color: ct.colorscheme.TableRow, + Text: symbol, + }) + case "price": + rowCells = append(rowCells, + &table.RowCell{ + LeftMargin: 1, + Width: 14, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: humanize.Commaf(coin.Price), + }) + case "holdings": + rowCells = append(rowCells, + &table.RowCell{ + LeftMargin: 1, + Width: 16, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: strconv.FormatFloat(coin.Holdings, 'f', -1, 64), + }) + case "balance": + rowCells = append(rowCells, + &table.RowCell{ + LeftMargin: 1, + Width: 16, + LeftAlign: false, + Color: colorbalance, + Text: humanize.Commaf(coin.Balance), + }) + case "1h_change": + rowCells = append(rowCells, + &table.RowCell{ + LeftMargin: 1, + Width: 10, + LeftAlign: false, + Color: color1h, + Text: fmt.Sprintf("%.2f%%", coin.PercentChange1H), + }) + case "24h_change": + rowCells = append(rowCells, + &table.RowCell{ + LeftMargin: 1, + Width: 10, + LeftAlign: false, + Color: color24h, + Text: fmt.Sprintf("%.2f%%", coin.PercentChange24H), + }) + case "7d_change": + rowCells = append(rowCells, + &table.RowCell{ + LeftMargin: 1, + Width: 10, + LeftAlign: false, + Color: color7d, + Text: fmt.Sprintf("%.2f%%", coin.PercentChange7D), + }) + case "percent_holdings": + rowCells = append(rowCells, + &table.RowCell{ + LeftMargin: 1, + Width: 14, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: fmt.Sprintf("%.2f%%", percentHoldings), + }) + case "last_updated": + rowCells = append(rowCells, + &table.RowCell{ + LeftMargin: 1, + Width: 18, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: lastUpdated, + }) + } + } + + t.AddRowCells(rowCells...) } return t diff --git a/cointop/price_alerts.go b/cointop/price_alerts.go index 8ae8f15a..b233101f 100644 --- a/cointop/price_alerts.go +++ b/cointop/price_alerts.go @@ -20,7 +20,7 @@ func (ct *Cointop) GetPriceAlertsTableHeaders() []string { return []string{ "name", "symbol", - "targetprice", + "target_price", "price", "frequency", } @@ -296,11 +296,11 @@ func (ct *Cointop) HidePriceAlertsUpdateMenu() error { // EnterKeyPressHandler is the key press handle for update menus func (ct *Cointop) EnterKeyPressHandler() error { - if ct.IsPortfolioVisible() { - return ct.SetPortfolioHoldings() + if ct.IsPriceAlertsVisible() { + return ct.CreatePriceAlert() } - return ct.CreatePriceAlert() + return ct.SetPortfolioHoldings() } // CreatePriceAlert sets price from inputed value