summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md69
-rw-r--r--cointop/actions.go16
-rw-r--r--cointop/chart.go11
-rw-r--r--cointop/coin.go6
-rw-r--r--cointop/cointop.go15
-rw-r--r--cointop/config.go134
-rw-r--r--cointop/conversion.go21
-rw-r--r--cointop/debug.go9
-rw-r--r--cointop/default_shortcuts.go5
-rw-r--r--cointop/favorites.go2
-rw-r--r--cointop/keybindings.go8
-rw-r--r--cointop/list.go16
-rw-r--r--cointop/marketbar.go12
-rw-r--r--cointop/navigation.go18
-rw-r--r--cointop/portfolio.go258
-rw-r--r--cointop/sort.go10
-rw-r--r--cointop/table_header.go25
-rw-r--r--docs/content/config.md3
-rw-r--r--docs/content/faq.md39
-rw-r--r--pkg/api/impl/coingecko/coingecko.go64
-rw-r--r--pkg/api/impl/coinmarketcap/coinmarketcap.go8
-rw-r--r--pkg/api/interface.go1
-rw-r--r--pkg/api/vendors/coingecko/v3/v3.go12
-rw-r--r--pkg/humanize/humanize.go5
-rw-r--r--pkg/pathutil/pathutil.go2
-rw-r--r--pkg/termui/linechart.go2
-rw-r--r--pkg/ui/ui.go6
27 files changed, 641 insertions, 136 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc7b173..e3c587b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.6.9] - 2021-10-12
+### Added
+- Chart x-axis date labels
+- Configurable favorite character
+- Configurable chart width
+- Save chart height
+
+### Changed
+- Renamed organization `miguelmota` → `cointop-sh`
+
+### Fixed
+- Global chart currency
+- Chart resampling and interpolation
+- Chart time periods
+- Use preferred cache directory
+- Currency symbol width
+
## [1.6.8] - 2021-09-13
### Fixed
- Hide holdings amount when using command hide flag
@@ -55,50 +72,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Config option to keep row focus on sort
## [1.6.1] - 2021-02-12
+### Added
+- Multiple coin support in price command
+
### Fixed
- Chart data interpolation
- CoinMarketCap graph data endpoint
+## [1.6.0] - 2021-02-12
### Added
-- Multiple coin support in price command
+- Configurable table columns
+- Basic price alerts
-## [1.6.0] - 2021-02-12
### Fixed
- Coin chart lookup
- Dynamic column widths
+## [1.5.5] - 2020-11-15
### Added
-- Configurable table columns
-- Basic price alerts
+- Currency convesion option to holdings command
+- Sort by percent holdings shortcut
-## [1.5.5] - 2020-11-15
### Fixed
- Termux cache directory
- Open command on Windows
+## [1.5.4] - 2020-08-24
### Added
-- Currency convesion option to holdings command
-- Sort by percent holdings shortcut
+- Colorschemes directory flag
-## [1.5.4] - 2020-08-24
### Fixed
- Rank order for low market cap coins
-### Added
-- Colorschemes directory flag
-
## [1.5.3] - 2020-08-14
### Fixed
- Build error
## [1.5.2] - 2020-08-13
-### Fixed
-- `XDG_CONFIG_HOME` config path
-
### Added
- Holdings command with sorting and filter options
- Bitcoin dominance command
+### Fixed
+- `XDG_CONFIG_HOME` config path
+
## [1.5.1] - 2020-08-05
### Fixed
- Version typo
@@ -124,24 +141,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Increase number of page results from CoinGecko
## [1.4.5] - 2020-02-18
-### Fixed
-- Convert to chosen currency for market data
-
### Added
- VND currency conversion
+### Fixed
+- Convert to chosen currency for market data
+
## [1.4.4] - 2019-12-31
### Fixed
- Flathub app release version
## [1.4.3] - 2019-12-29
+### Added
+- Tab keybinding
+
### Fixed
- Chart update bug fixes
- Marketbar currency bug fixes
-### Added
-- Tab keybinding
-
## [1.4.2] - 2019-12-29
### Fixed
- Fix keybinding issue on FreeBSD
@@ -209,25 +226,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Release archive to contain latest source code
## [1.1.4] - 2019-04-21
-### Changed
-- CoinMarketCap legacy V2 API to Pro V1 API
-
### Added
- Config option to use CoinMarketCap Pro V1 API KEY
+### Changed
+- CoinMarketCap legacy V2 API to Pro V1 API
+
## [1.1.3] - 2019-02-25
### Fixed
- Vendor dependencies
## [1.1.2] - 2018-12-30
-### Fixed
-- Paginate CoinMarketCap V1 API responses due to their backward-incompatible update
-
### Added
- `-clean` flag to clean cache
- `-reset` flag to clean cache and delete config
- `-config` flag to use a different specified config file
+### Fixed
+- Paginate CoinMarketCap V1 API responses due to their backward-incompatible update
+
## [1.1.1] - 2018-12-26
### Changed
- Use go modules instead of dep
diff --git a/cointop/actions.go b/cointop/actions.go
index 53f098b..a05ac57 100644
--- a/cointop/actions.go
+++ b/cointop/actions.go
@@ -4,6 +4,7 @@ package cointop
func ActionsMap() map[string]bool {
return map[string]bool{
"first_page": true,
+ "move_to_first_page_first_row": true,
"help": true,
"toggle_show_help": true,
"close_help": true,
@@ -56,6 +57,21 @@ func ActionsMap() map[string]bool {
"toggle_show_portfolio": true,
"enlarge_chart": true,
"shorten_chart": true,
+ "toggle_chart_fullscreen": true,
+ "scroll_right": true,
+ "show_portfolio_edit_menu": true,
+ "sort_column_percent_holdings": true,
+ "toggle_portfolio_balances": true,
+ "scroll_left": true,
+ "save": true,
+ "toggle_table_fullscreen": true,
+ "toggle_price_alerts": true,
+ "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,
}
}
diff --git a/cointop/chart.go b/cointop/chart.go
index dcca3f4..98ff63c 100644
--- a/cointop/chart.go
+++ b/cointop/chart.go
@@ -315,10 +315,15 @@ func (ct *Cointop) PortfolioChart() error {
// Scale Portfolio Balances to hide value
if ct.State.hidePortfolioBalances {
- var lastPrice = data[len(data)-1]
- if lastPrice > 0.0 {
+ scalePrice := 0.0
+ for _, price := range data {
+ if price > scalePrice {
+ scalePrice = price
+ }
+ }
+ if scalePrice > 0.0 {
for i, price := range data {
- data[i] = 100 * price / lastPrice
+ data[i] = 100 * price / scalePrice
}
}
}
diff --git a/cointop/coin.go b/cointop/coin.go
index ef8f4fd..861f7cb 100644
--- a/cointop/coin.go
+++ b/cointop/coin.go
@@ -23,8 +23,10 @@ type Coin struct {
// for favorites
Favorite bool
// for portfolio
- Holdings float64
- Balance float64
+ Holdings float64
+ Balance float64
+ BuyPrice float64
+ BuyCurrency string
}
// AllCoins returns a slice of all the coins
diff --git a/cointop/cointop.go b/cointop/cointop.go
index 4b15410..d4579ed 100644
--- a/cointop/cointop.go
+++ b/cointop/cointop.go
@@ -92,6 +92,7 @@ type State struct {
tableCompactNotation bool
favoritesCompactNotation bool
portfolioCompactNotation bool
+ enableMouse bool
}
// Cointop cointop
@@ -125,8 +126,10 @@ type Cointop struct {
// PortfolioEntry is portfolio entry
type PortfolioEntry struct {
- Coin string
- Holdings float64
+ 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
@@ -200,7 +206,7 @@ var DefaultSortBy = "rank"
var DefaultPerPage = uint(100)
// DefaultMaxPages ...
-var DefaultMaxPages = uint(35)
+var DefaultMaxPages = uint(10)
// DefaultColorscheme ...
var DefaultColorscheme = "cointop"
@@ -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 {
diff --git a/cointop/config.go b/cointop/config.go
index 58c467e..40b7af3 100644
--- a/cointop/config.go
+++ b/cointop/config.go
@@ -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
@@ -366,14 +372,44 @@ func (ct *Cointop) loadTableColumnsFromConfig() error {
// LoadShortcutsFromConfig loads keyboard shortcuts from config file to struct
func (ct *Cointop) loadShortcutsFromConfig() error {
log.Debug("loadShortcutsFromConfig()")
+
+ // Load the shortcut config into a key:action map (filtering to actions that exist). Keep track of actions.
+ config := make(map[string]string)
+ actions := make(map[string]bool)
for k, ifc := range ct.config.Shortcuts {
if v, ok := ifc.(string); ok {
if !ct.ActionExists(v) {
+ log.Debugf("Shortcut '%s'=>%s is not a valid action", k, v)
continue
}
- ct.State.shortcutKeys[k] = v
+ config[k] = v
+ actions[v] = true
}
}
+
+ // Count how many keys are configured per action.
+ actionCount := make(map[string]int)
+ for _, action := range ct.State.shortcutKeys {
+ actionCount[action] += 1
+ }
+
+ // merge defaults into the loaded config - if the key is not defined, and the action is not found, add it
+ for key, action := range ct.State.shortcutKeys {
+ if _, ok := config[key]; ok {
+ // k is already in the config - ignore it
+ } else if _, ok := actions[action]; ok {
+ if actionCount[action] == 1 {
+ // action is already in the config - ignore it
+ } else {
+ // there are multiple bindings, add them anyway
+ config[key] = action // add action
+ }
+ } else {
+ config[key] = action // add action
+ }
+ }
+ ct.State.shortcutKeys = config
+
return nil
}
@@ -476,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()")
@@ -554,33 +600,7 @@ func (ct *Cointop) loadPortfolioFromConfig() error {
}
}
} else if key == "holdings" {
- holdingsIfc, ok := valueIfc.([]interface{})
- if !ok {
- continue
- }
-
- 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
- }
- }
+ // Defer until the end to work around premature-save issue
} else if key == "compact_notation" {
ct.State.portfolioCompactNotation = valueIfc.(bool)
} else {
@@ -590,12 +610,62 @@ func (ct *Cointop) loadPortfolioFromConfig() error {
return err
}
- if err := ct.SetPortfolioEntry(key, holdings); err != nil {
+ 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) > 4 {
+ continue
+ }
+ name, ok := tupleIfc[0].(string)
+ if !ok {
+ continue // was not a string
+ }
+
+ holdings, err := ct.InterfaceToFloat64(tupleIfc[1])
+ if err != nil {
+ return err // was not a float64
+ }
+
+ buyPrice := 0.0
+ if len(tupleIfc) >= 3 {
+ if buyPrice, err = ct.InterfaceToFloat64(tupleIfc[2]); err != nil {
+ return err
+ }
+ }
+
+ buyCurrency := ""
+ if len(tupleIfc) >= 4 {
+ if parseCurrency, ok := tupleIfc[3].(string); !ok {
+ return err // was not a string
+ } else {
+ buyCurrency = parseCurrency
+ }
+ }
+
+ // 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
}
diff --git a/cointop/conversion.go b/cointop/conversion.go
index 7094ad7..bbe2a2e 100644
--- a/cointop/conversion.go
+++ b/cointop/conversion.go
@@ -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",
@@ -241,7 +241,7 @@ func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error {
if err := ct.Save(); err != nil {
return err
}
-
+ go ct.UpdateCurrentPageCoins()
go ct.RefreshAll()
return nil
}
@@ -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
+}
diff --git a/cointop/debug.go b/cointop/debug.go
index aab5dee..631168c 100644
--- a/cointop/debug.go
+++ b/cointop/debug.go
@@ -1,13 +1,22 @@
package cointop
import (
+ "fmt"
"os"
+ "github.com/cointop-sh/cointop/pkg/pathutil"
log "github.com/sirupsen/logrus"
)
func (ct *Cointop) initlog() {
filename := "/tmp/cointop.log"
+ debugFile := os.Getenv("DEBUG_FILE")
+ if debugFile != "" {
+ filename = pathutil.NormalizePath(debugFile)
+ if filename != debugFile && os.Getenv("DEBUG") != "" {
+ fmt.Printf("Writing debug log to %s\n", filename)
+ }
+ }
f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
panic(err)
diff --git a/cointop/default_shortcuts.go b/cointop/default_shortcuts.go
index 8dc8ff1..e18988d 100644
--- a/cointop/default_shortcuts.go
+++ b/cointop/default_shortcuts.go
@@ -36,7 +36,7 @@ func DefaultShortcuts() map[string]string {
"alt+right": "sort_right_column",
"F1": "help",
"F5": "refresh",
- "0": "first_page",
+ "0": "move_to_first_page_first_row",
"1": "sort_column_1h_change",
"2": "sort_column_24h_change",
"3": "sort_column_30d_change",
@@ -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",
}
}
diff --git a/cointop/favorites.go b/cointop/favorites.go
index a2907b6..2aa4815 100644
--- a/cointop/favorites.go
+++ b/cointop/favorites.go
@@ -64,7 +64,7 @@ func (ct *Cointop) GetFavoritesSlice() []*Coin {
}
}
- sort.Slice(sliced, func(i, j int) bool {
+ sort.SliceStable(sliced, func(i, j int) bool {
return sliced[i].MarketCap > sliced[j].MarketCap
})
diff --git a/cointop/keybindings.go b/cointop/keybindings.go
index 2f0b6b3..b220901 100644
--- a/cointop/keybindings.go
+++ b/cointop/keybindings.go
@@ -218,6 +218,8 @@ func (ct *Cointop) SetKeybindingAction(shortcutKey string, action string) error
view = "help"
case "first_page":
fn = ct.Keyfn(ct.FirstPage)
+ case "move_to_first_page_first_row":
+ fn = ct.Keyfn(ct.NavigateToFirstPageFirstRow)
case "sort_column_1h_change":
fn = ct.Sortfn("1h_change", true)
case "sort_column_24h_change":
@@ -323,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)
}
diff --git a/cointop/list.go b/cointop/list.go
index 94d28cd..ff66e6a 100644
--- a/cointop/list.go
+++ b/cointop/list.go
@@ -46,6 +46,22 @@ func (ct *Cointop) UpdateCoins() error {
return nil
}
+// UpdateCurrentPageCoins updates all the coins in the current page
+func (ct *Cointop) UpdateCurrentPageCoins() error {
+ log.Debugf("UpdateCurrentPageCoins(%d)", len(ct.State.coins))
+ currentPageCoins := make([]string, len(ct.State.coins))
+ for i, entry := range ct.State.coins {
+ currentPageCoins[i] = entry.Name
+ }
+
+ coins, err := ct.api.GetCoinDataBatch(currentPageCoins, ct.State.currencyConversion)
+ if err != nil {
+ return err
+ }
+ go ct.processCoins(coins)
+ return nil
+}
+
// ProcessCoinsMap processes coins map
func (ct *Cointop) processCoinsMap(coinsMap map[string]types.Coin) {
log.Debug("ProcessCoinsMap()")
diff --git a/cointop/marketbar.go b/cointop/marketbar.go
index 86b5dbf..bfaeb2c 100644
--- a/cointop/marketbar.go
+++ b/cointop/marketbar.go
@@ -78,10 +78,9 @@ func (ct *Cointop) UpdateMarketbar() error {
chartInfo := ""
if !ct.State.hideChart {
chartInfo = fmt.Sprintf(
- "[ Chart: %s %s %s ] ",
+ "[ Chart: %s %s ] ",
charttitle,
timeframe,
- ct.State.currencyConversion,
)
}
@@ -92,8 +91,9 @@ func (ct *Cointop) UpdateMarketbar() error {
}
content = fmt.Sprintf(
- "%sTotal Portfolio Value: %s • 24H: %s",
+ "%sTotal Portfolio Value %s: %s • 24H: %s",
chartInfo,
+ ct.State.currencyConversion,
ct.colorscheme.MarketBarLabelActive(totalstr),
percentChange24Hstr,
)
@@ -142,10 +142,9 @@ func (ct *Cointop) UpdateMarketbar() error {
chartInfo := ""
if !ct.State.hideChart {
chartInfo = fmt.Sprintf(
- "[ Chart: %s %s %s] ",
+ "[ Chart: %s %s] ",
ct.colorscheme.MarketBarLabelActive(chartname),
timeframe,
- ct.State.currencyConversion,
)
}
@@ -166,8 +165,9 @@ func (ct *Cointop) UpdateMarketbar() error {
}
content = fmt.Sprintf(
- "%sGlobal ▶ Market Cap: %s %s 24H Volume: %s %s BTC Dominance: %.2f%%",
+ "%sGlobal %s ▶ Market Cap: %s %s 24H Volume: %s %s BTC Dominance: %.2f%%",
chartInfo,
+ ct.State.currencyConversion,
fmt.Sprintf("%s%s", ct.CurrencySymbol(), marketCapStr),
separator1,
fmt.Sprintf("%s%s", ct.CurrencySymbol(), volumeStr),
diff --git a/cointop/navigation.go b/cointop/navigation.go
index abbd2be..e5b4b2e 100644
--- a/cointop/navigation.go
+++ b/cointop/navigation.go
@@ -309,6 +309,13 @@ func (ct *Cointop) PrevPageTop() error {
return nil
}
+// NavigateToFirstPageFirstRow navigates to the first row on the first page
+func (ct *Cointop) NavigateToFirstPageFirstRow() error {
+ log.Debug("TopCoin()")
+ ct.GoToGlobalIndex(0)
+ return nil
+}
+
// FirstPage navigates to the first page
func (ct *Cointop) FirstPage() error {
log.Debug("FirstPage()")
@@ -409,13 +416,18 @@ func (ct *Cointop) GoToPageRowIndex(idx int) error {
// GoToGlobalIndex navigates to the selected row index of all page rows
func (ct *Cointop) GoToGlobalIndex(idx int) error {
- log.Debug("GoToGlobalIndex()")
+ log.Debugf("GoToGlobalIndex(%d)", idx)
+ target := ct.State.allCoins[idx]
l := ct.TableRowsLen()
atpage := idx / l
ct.SetPage(atpage)
- rowIndex := idx % l
- ct.HighlightRow(rowIndex)
ct.UpdateTable()
+ // Look for the coin in the current page
+ for i, coin := range ct.State.coins {
+ if coin == target {
+ ct.HighlightRow(i)
+ }
+ }
return nil
}
diff --git a/cointop/portfolio.go b/cointop/portfolio.go
index d27f32f..f6613fb 100644
--- a/cointop/portfolio.go
+++ b/cointop/portfolio.go
@@ -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
+