From ba75de3c00a0a1f37d0087bcf7b84333de9d5e00 Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Mon, 23 Aug 2021 17:45:08 +1000 Subject: [PATCH 1/8] Cache ct.State.selectedChartRange and ct.State.currencyConversion so data-fetch is not impacted by concurrent change --- cointop/chart.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cointop/chart.go b/cointop/chart.go index a566b75..f9e543f 100644 --- a/cointop/chart.go +++ b/cointop/chart.go @@ -189,8 +189,10 @@ func (ct *Cointop) PortfolioChart() error { chart := chartplot.NewChartPlot() chart.SetHeight(ct.State.chartHeight) - rangeseconds := ct.chartRangesMap[ct.State.selectedChartRange] - if ct.State.selectedChartRange == "YTD" { + convert := ct.State.currencyConversion // cache here + selectedChartRange := ct.State.selectedChartRange // cache here + rangeseconds := ct.chartRangesMap[selectedChartRange] + if selectedChartRange == "YTD" { ytd := time.Now().Unix() - int64(timeutil.BeginningOfYear().Unix()) rangeseconds = time.Duration(ytd) * time.Second } @@ -216,7 +218,7 @@ func (ct *Cointop) PortfolioChart() error { } var graphData []float64 - cachekey := strings.ToLower(fmt.Sprintf("%s_%s", p.Symbol, strings.Replace(ct.State.selectedChartRange, " ", "", -1))) + cachekey := strings.ToLower(fmt.Sprintf("%s_%s", p.Symbol, strings.Replace(selectedChartRange, " ", "", -1))) cached, found := ct.cache.Get(cachekey) if found { // cache hit @@ -230,7 +232,6 @@ func (ct *Cointop) PortfolioChart() error { if len(graphData) == 0 { time.Sleep(2 * time.Second) - convert := ct.State.currencyConversion apiGraphData, err := ct.api.GetCoinGraphData(convert, p.Symbol, p.Name, start, end) if err != nil { return err From a34417ad61518d570afcd6a12f12ded2d0556fd8 Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Wed, 18 Aug 2021 11:51:35 +1000 Subject: [PATCH 2/8] When building portfolio slice, include first coin only --- cointop/portfolio.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cointop/portfolio.go b/cointop/portfolio.go index 58ab85c..d46b980 100644 --- a/cointop/portfolio.go +++ b/cointop/portfolio.go @@ -529,12 +529,20 @@ func (ct *Cointop) GetPortfolioSlice() []*Coin { return sliced } +OUTER: for i := range ct.State.allCoins { coin := ct.State.allCoins[i] p, isNew := ct.PortfolioEntry(coin) if isNew { continue } + // check not already found + for j := range sliced { + if coin.Symbol == sliced[j].Symbol { + continue OUTER + } + } + coin.Holdings = p.Holdings balance := coin.Price * p.Holdings balancestr := fmt.Sprintf("%.2f", balance) From b078dbd2f67cd1483f0207e0147f537710fdf679 Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Thu, 19 Aug 2021 11:54:02 +1000 Subject: [PATCH 3/8] Use the highest-rank coin to calculate PortfolioSlice --- cointop/portfolio.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cointop/portfolio.go b/cointop/portfolio.go index d46b980..93a89dc 100644 --- a/cointop/portfolio.go +++ b/cointop/portfolio.go @@ -537,9 +537,14 @@ OUTER: continue } // check not already found + updateSlice := -1 for j := range sliced { if coin.Symbol == sliced[j].Symbol { - continue OUTER + if coin.Rank >= sliced[j].Rank { + continue OUTER // skip updates from lower-ranked coins + } + updateSlice = j // update this later + break } } @@ -551,7 +556,12 @@ OUTER: } balance, _ = strconv.ParseFloat(balancestr, 64) coin.Balance = balance - sliced = append(sliced, coin) + if updateSlice == -1 { + sliced = append(sliced, coin) + } else { + sliced[updateSlice] = coin + } + } sort.Slice(sliced, func(i, j int) bool { From b83d15cdc106290d7d7d7b6c07c8f5d25224cd5c Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Thu, 19 Aug 2021 09:05:39 +1000 Subject: [PATCH 4/8] Store default chart range in configuration file --- cointop/cointop.go | 1 + cointop/config.go | 69 ++++++++++++++++++++++++++++++---------------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/cointop/cointop.go b/cointop/cointop.go index 40dccf8..1f16367 100644 --- a/cointop/cointop.go +++ b/cointop/cointop.go @@ -44,6 +44,7 @@ type State struct { coinsTableColumns []string convertMenuVisible bool defaultView string + defaultChartRange string // DEPRECATED: favorites by 'symbol' is deprecated because of collisions. favoritesBySymbol map[string]bool diff --git a/cointop/config.go b/cointop/config.go index 9464e4d..e158768 100644 --- a/cointop/config.go +++ b/cointop/config.go @@ -31,18 +31,19 @@ var possibleConfigPaths = []string{ } type config struct { - Shortcuts map[string]interface{} `toml:"shortcuts"` - Favorites map[string]interface{} `toml:"favorites"` - Portfolio map[string]interface{} `toml:"portfolio"` - PriceAlerts map[string]interface{} `toml:"price_alerts"` - Currency interface{} `toml:"currency"` - DefaultView interface{} `toml:"default_view"` - CoinMarketCap map[string]interface{} `toml:"coinmarketcap"` - API interface{} `toml:"api"` - Colorscheme interface{} `toml:"colorscheme"` - RefreshRate interface{} `toml:"refresh_rate"` - CacheDir interface{} `toml:"cache_dir"` - Table map[string]interface{} `toml:"table"` + Shortcuts map[string]interface{} `toml:"shortcuts"` + Favorites map[string]interface{} `toml:"favorites"` + Portfolio map[string]interface{} `toml:"portfolio"` + PriceAlerts map[string]interface{} `toml:"price_alerts"` + Currency interface{} `toml:"currency"` + DefaultView interface{} `toml:"default_view"` + DefaultChartRange interface{} `toml:"default_chart_range"` + CoinMarketCap map[string]interface{} `toml:"coinmarketcap"` + API interface{} `toml:"api"` + Colorscheme interface{} `toml:"colorscheme"` + RefreshRate interface{} `toml:"refresh_rate"` + CacheDir interface{} `toml:"cache_dir"` + Table map[string]interface{} `toml:"table"` } // SetupConfig loads config file @@ -69,6 +70,9 @@ func (ct *Cointop) SetupConfig() error { if err := ct.loadDefaultViewFromConfig(); err != nil { return err } + if err := ct.loadDefaultChartRangeFromConfig(); err != nil { + return err + } if err := ct.loadAPIKeysFromConfig(); err != nil { return err } @@ -255,6 +259,7 @@ func (ct *Cointop) configToToml() ([]byte, error) { var currencyIfc interface{} = ct.State.currencyConversion var defaultViewIfc interface{} = ct.State.defaultView + var defaultChartRangeIfc interface{} = ct.State.defaultChartRange var colorschemeIfc interface{} = ct.colorschemeName var refreshRateIfc interface{} = uint(ct.State.refreshRate.Seconds()) var cacheDirIfc interface{} = ct.State.cacheDir @@ -289,18 +294,19 @@ func (ct *Cointop) configToToml() ([]byte, error) { tableMapIfc["keep_row_focus_on_sort"] = keepRowFocusOnSortIfc var inputs = &config{ - API: apiChoiceIfc, - Colorscheme: colorschemeIfc, - CoinMarketCap: cmcIfc, - Currency: currencyIfc, - DefaultView: defaultViewIfc, - Favorites: favoritesMapIfc, - RefreshRate: refreshRateIfc, - Shortcuts: shortcutsIfcs, - Portfolio: portfolioIfc, - PriceAlerts: priceAlertsMapIfc, - CacheDir: cacheDirIfc, - Table: tableMapIfc, + API: apiChoiceIfc, + Colorscheme: colorschemeIfc, + CoinMarketCap: cmcIfc, + Currency: currencyIfc, + DefaultView: defaultViewIfc, + DefaultChartRange: defaultChartRangeIfc, + Favorites: favoritesMapIfc, + RefreshRate: refreshRateIfc, + Shortcuts: shortcutsIfcs, + Portfolio: portfolioIfc, + PriceAlerts: priceAlertsMapIfc, + CacheDir: cacheDirIfc, + Table: tableMapIfc, } var b bytes.Buffer @@ -399,6 +405,21 @@ func (ct *Cointop) loadDefaultViewFromConfig() error { return nil } +// LoadDefaultChartRangeFromConfig loads default chart range from config file to struct +func (ct *Cointop) loadDefaultChartRangeFromConfig() error { + ct.debuglog("loadDefaultChartRangeFromConfig()") + if defaultChartRange, ok := ct.config.DefaultChartRange.(string); ok { + // validate configured value + _, present := ct.chartRangesMap[defaultChartRange] + if !present { + defaultChartRange = DefaultChartRange + } + ct.State.defaultChartRange = defaultChartRange + ct.State.selectedChartRange = defaultChartRange + } + return nil +} + // LoadAPIKeysFromConfig loads API keys from config file to struct func (ct *Cointop) loadAPIKeysFromConfig() error { ct.debuglog("loadAPIKeysFromConfig()") From 142777d965b3ecd12b96b31dede13323195efcc8 Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Thu, 19 Aug 2021 09:06:16 +1000 Subject: [PATCH 5/8] Simple documentation for default_chart_range --- docs/content/config.md | 1 + docs/content/faq.md | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/content/config.md b/docs/content/config.md index 275aea0..6c6dac2 100644 --- a/docs/content/config.md +++ b/docs/content/config.md @@ -39,6 +39,7 @@ You can configure the actions you want for each key in `config.toml`: ```toml currency = "USD" default_view = "" +default_chart_range = "1Y" api = "coingecko" colorscheme = "cointop" refresh_rate = 60 diff --git a/docs/content/faq.md b/docs/content/faq.md index 65c42bc..605e2f3 100644 --- a/docs/content/faq.md +++ b/docs/content/faq.md @@ -328,6 +328,12 @@ draft: false In the config file, set `default_view = "default"` +## How do I set the default chart range? + + In the config file, set `default_chart_range = "3M"` + + Supported date ranges are `All Time`, `YTD`, `1Y`, `6M`, `3M`, `1M`, `7D`, `3D`, `24H`. + ## How can use a different config file other than the default? Run cointop with the `--config` flag, eg `cointop --config="/path/to/config.toml"`, to use the specified file as the config. From e60bc6dcd6ea615f866c2989d76b1f766a92984f Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Thu, 19 Aug 2021 16:40:54 +1000 Subject: [PATCH 6/8] Include DefaultChartRange in default config --- cointop/cointop.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cointop/cointop.go b/cointop/cointop.go index 1f16367..17a9843 100644 --- a/cointop/cointop.go +++ b/cointop/cointop.go @@ -244,6 +244,7 @@ func NewCointop(config *Config) (*Cointop, error) { cacheDir: DefaultCacheDir, coinsTableColumns: DefaultCoinTableHeaders, currencyConversion: DefaultCurrency, + defaultChartRange: DefaultChartRange, // DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility. favoritesBySymbol: make(map[string]bool), favorites: make(map[string]bool), From 28a7bfbbd9ccb658f92999a003a2e77a7faff4d3 Mon Sep 17 00:00:00 2001 From: Miguel Mota Date: Sun, 22 Aug 2021 04:27:41 -0700 Subject: [PATCH 7/8] Return error if default chart range is invalid --- cointop/config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cointop/config.go b/cointop/config.go index e158768..0f379c2 100644 --- a/cointop/config.go +++ b/cointop/config.go @@ -410,9 +410,9 @@ func (ct *Cointop) loadDefaultChartRangeFromConfig() error { ct.debuglog("loadDefaultChartRangeFromConfig()") if defaultChartRange, ok := ct.config.DefaultChartRange.(string); ok { // validate configured value - _, present := ct.chartRangesMap[defaultChartRange] - if !present { - defaultChartRange = DefaultChartRange + _, ok := ct.chartRangesMap[defaultChartRange] + if !ok { + return fmt.Errorf("invalid default chart range %q. Valid ranges are: %s", defaultChartRange, strings.Join(ChartRanges(), ",")) } ct.State.defaultChartRange = defaultChartRange ct.State.selectedChartRange = defaultChartRange From b32da4010c5a5cb4c1a7ec80cb0d83bf7b9063ef Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Thu, 26 Aug 2021 09:44:11 +1000 Subject: [PATCH 8/8] Include currencyConversion in the cache key so changing currencies invalidates the cache. See https://github.com/miguelmota/cointop/issues/144 --- cointop/chart.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cointop/chart.go b/cointop/chart.go index ecade65..363afed 100644 --- a/cointop/chart.go +++ b/cointop/chart.go @@ -218,7 +218,7 @@ func (ct *Cointop) PortfolioChart() error { } var graphData []float64 - cachekey := strings.ToLower(fmt.Sprintf("%s_%s", p.Symbol, strings.Replace(selectedChartRange, " ", "", -1))) + cachekey := strings.ToLower(fmt.Sprintf("%s_%s_%s", p.Symbol, convert, strings.Replace(selectedChartRange, " ", "", -1))) cached, found := ct.cache.Get(cachekey) if found { // cache hit