Merge branch 'master' into numeric-monetary-locale

pull/112/head
afh 3 years ago
commit 6f92573a93

@ -12,12 +12,14 @@ func HoldingsCmd() *cobra.Command {
var help bool
var total bool
var noCache bool
var noHeader bool
var config string
var sortBy string
var sortDesc bool
var format string = "table"
var humanReadable bool
var filter []string
var cols []string
var convert string
holdingsCmd := &cobra.Command{
@ -52,7 +54,9 @@ func HoldingsCmd() *cobra.Command {
HumanReadable: humanReadable,
Format: format,
Filter: filter,
Cols: cols,
Convert: convert,
NoHeader: noHeader,
})
},
}
@ -61,11 +65,13 @@ func HoldingsCmd() *cobra.Command {
holdingsCmd.Flags().BoolVarP(&total, "total", "t", total, "Show total only")
holdingsCmd.Flags().BoolVarP(&noCache, "no-cache", "", noCache, "No cache")
holdingsCmd.Flags().BoolVarP(&humanReadable, "human", "h", humanReadable, "Human readable output")
holdingsCmd.Flags().BoolVarP(&noHeader, "no-header", "", noHeader, "Don't display header columns")
holdingsCmd.Flags().StringVarP(&config, "config", "c", "", fmt.Sprintf("Config filepath. (default %s)", cointop.DefaultConfigFilepath))
holdingsCmd.Flags().StringVarP(&sortBy, "sort-by", "s", sortBy, `Sort by column. Options are "name", "symbol", "price", "holdings", "balance", "24h"`)
holdingsCmd.Flags().BoolVarP(&sortDesc, "sort-desc", "d", sortDesc, "Sort in descending order")
holdingsCmd.Flags().StringVarP(&format, "format", "", format, `Ouput format. Options are "table", "csv", "json"`)
holdingsCmd.Flags().StringSliceVarP(&filter, "filter", "", filter, `Filter portfolio entries by coin name or symbol, comma separated. Example: "btc,eth,doge"`)
holdingsCmd.Flags().StringSliceVarP(&filter, "filter", "", filter, `Filter portfolio entries by coin name or symbol, comma separated without spaces. Example: "btc,eth,doge"`)
holdingsCmd.Flags().StringSliceVarP(&cols, "cols", "", cols, `Filter portfolio columns, comma separated without spaces. Example: "symbol,holdings,balance"`)
holdingsCmd.Flags().StringVarP(&convert, "convert", "f", convert, "The currency to convert to")
return holdingsCmd

@ -1,6 +1,7 @@
package cointop
import (
"crypto/sha256"
"fmt"
"io/ioutil"
"os"
@ -307,8 +308,11 @@ func NewCointop(config *Config) (*Cointop, error) {
}
if !config.NoCache {
// each custom config file has it's own file cache
hash := sha256.Sum256([]byte(ct.ConfigFilePath()))
fcache, err := filecache.NewFileCache(&filecache.Config{
CacheDir: ct.State.cacheDir,
Prefix: fmt.Sprintf("%x", hash[0:4]),
})
if err != nil {
fmt.Printf("error: %s\nyou may change the cache directory with --cache-dir flag.\nproceeding without filecache.\n", err)

@ -98,11 +98,16 @@ func (ct *Cointop) SetupConfig() error {
func (ct *Cointop) CreateConfigIfNotExists() error {
ct.debuglog("createConfigIfNotExists()")
for _, configPath := range possibleConfigPaths {
normalizedPath := pathutil.NormalizePath(configPath)
if _, err := os.Stat(normalizedPath); err == nil {
ct.configFilepath = normalizedPath
return nil
ct.configFilepath = pathutil.NormalizePath(ct.configFilepath)
// check if config file exists in one of th default paths
if ct.configFilepath == DefaultConfigFilepath {
for _, configPath := range possibleConfigPaths {
normalizedPath := pathutil.NormalizePath(configPath)
if _, err := os.Stat(normalizedPath); err == nil {
ct.configFilepath = normalizedPath
return nil
}
}
}

@ -591,7 +591,9 @@ type TablePrintOptions struct {
HumanReadable bool
Format string
Filter []string
Cols []string
Convert string
NoHeader bool
}
// outputFormats is list of valid output formats
@ -628,8 +630,10 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
sortDesc := options.SortDesc
format := options.Format
humanReadable := options.HumanReadable
filter := options.Filter
filterCoins := options.Filter
filterCols := options.Cols
holdings := ct.GetPortfolioSlice()
noHeader := options.NoHeader
if format == "" {
format = "table"
@ -651,10 +655,41 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
records := make([][]string, len(holdings))
symbol := ct.CurrencySymbol()
headers := []string{"name", "symbol", "price", "holdings", "balance", "24h%", "%holdings"}
if len(filterCols) > 0 {
for _, col := range filterCols {
valid := false
for _, h := range headers {
if col == h {
valid = true
break
}
}
switch col {
case "amount":
return fmt.Errorf("did you mean %q?", "balance")
case "24H":
fallthrough
case "24H%":
fallthrough
case "24h":
fallthrough
case "24h_change":
return fmt.Errorf("did you mean %q?", "24h%")
case "percent_holdings":
return fmt.Errorf("did you mean %q?", "%holdings")
}
if !valid {
return fmt.Errorf("unsupported column value %q", col)
}
}
headers = filterCols
}
for i, entry := range holdings {
if len(filter) > 0 {
if len(filterCoins) > 0 {
found := false
for _, item := range filter {
for _, item := range filterCoins {
item = strings.ToLower(strings.TrimSpace(item))
if strings.ToLower(entry.Symbol) == item || strings.ToLower(entry.Name) == item {
found = true
@ -671,35 +706,54 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
percentHoldings = 0
}
if humanReadable {
records[i] = []string{
entry.Name,
entry.Symbol,
fmt.Sprintf("%s%s", symbol, humanize.Monetaryf(entry.Price, 2)),
humanize.Numericf(entry.Holdings, 2),
fmt.Sprintf("%s%s", symbol, humanize.Monetaryf(entry.Balance, 2)),
humanize.Numericf(entry.PercentChange24H, 2),
humanize.Numericf(percentHoldings, 2),
}
} else {
records[i] = []string{
entry.Name,
entry.Symbol,
strconv.FormatFloat(entry.Price, 'f', -1, 64),
strconv.FormatFloat(entry.Holdings, 'f', -1, 64),
strconv.FormatFloat(entry.Balance, 'f', -1, 64),
fmt.Sprintf("%.2f", entry.PercentChange24H),
fmt.Sprintf("%.2f", percentHoldings),
item := make([]string, len(headers))
for i, header := range headers {
switch header {
case "name":
item[i] = entry.Name
case "symbol":
item[i] = entry.Symbol
case "price":
if humanReadable {
item[i] = fmt.Sprintf("%s%s", symbol, humanize.Monetaryf(entry.Price, 2))
} else {
item[i] = strconv.FormatFloat(entry.Price, 'f', -1, 64)
}
case "holdings":
if humanReadable {
item[i] = humanize.Monetaryf(entry.Holdings, 2)
} else {
item[i] = strconv.FormatFloat(entry.Holdings, 'f', -1, 64)
}
case "balance":
if humanReadable {
item[i] = fmt.Sprintf("%s%s", symbol, humanize.Monetaryf(entry.Balance, 2))
} else {
item[i] = strconv.FormatFloat(entry.Balance, 'f', -1, 64)
}
case "24h%":
if humanReadable {
item[i] = fmt.Sprintf("%s%%", humanize.Numericf(entry.PercentChange24H, 2))
} else {
item[i] = fmt.Sprintf("%.2f", entry.PercentChange24H)
}
case "%holdings":
if humanReadable {
item[i] = fmt.Sprintf("%s%%", humanize.Numericf(percentHoldings, 2))
} else {
item[i] = fmt.Sprintf("%.2f", percentHoldings)
}
}
}
records[i] = item
}
headers := []string{"name", "symbol", "price", "holdings", "balance", "24h%", "%holdings"}
if format == "csv" {
csvWriter := csv.NewWriter(os.Stdout)
if err := csvWriter.Write(headers); err != nil {
return err
if !noHeader {
if err := csvWriter.Write(headers); err != nil {
return err
}
}
for _, record := range records {
@ -715,19 +769,28 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
return nil
} else if format == "json" {
list := make([]map[string]string, len(records))
for i, record := range records {
obj := make(map[string]string, len(record))
for j, column := range record {
obj[headers[j]] = column
var output []byte
var err error
if noHeader {
output, err = json.Marshal(records)
if err != nil {
return err
}
} else {
list := make([]map[string]string, len(records))
for i, record := range records {
obj := make(map[string]string, len(record))
for j, column := range record {
obj[headers[j]] = column
}
list[i] = obj
}
list[i] = obj
}
output, err := json.Marshal(list)
if err != nil {
return err
output, err = json.Marshal(list)
if err != nil {
return err
}
}
fmt.Println(string(output))
@ -735,9 +798,13 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
}
alignment := []int{-1, -1, 1, 1, 1, 1, 1}
var tableHeaders []string
if !noHeader {
tableHeaders = headers
}
table := asciitable.NewAsciiTable(&asciitable.Input{
Data: records,
Headers: headers,
Headers: tableHeaders,
Alignment: alignment,
})

@ -0,0 +1,133 @@
---
title: "Portfolio"
date: 2020-01-01T00:00:00-00:00
draft: false
---
# Portfolio
<img src="https://user-images.githubusercontent.com/168240/50439364-a78ade00-08a6-11e9-992b-af63ef21100d.png" alt="portfolio screenshot" width="880" />
## View portfolio
To view your portfolio, press <kbd>P</kbd> (Shift+p)
## Exit portfolio
To exit out of the portfolio view press, <kbd>P</kbd> (Shift+p) again or <kbd>q</kbd> or <kbd>ESC</kbd>
## Add entry
To add a coin to your portfolio, press <kbd>e</kbd> on the highlighted coin, enter a value, and then press <kbd>Enter</kbd>
## Edit entry
To edit the holdings of coin in your portfolio, press <kbd>e</kbd> on the highlighted coin, enter the new value, and then press <kbd>Enter</kbd>
## Remove Entry
To remove an entry in your portfolio, press <kbd>e</kbd> on the highlighted coin and set the value to an empty value and press <kbd>Enter</kbd>
## Changing chart
To change the coin for the chart, press <kbd>Enter</kbd> on the highlighted coin. Pressing <kbd>Enter</kbd> again on the same highlighted row will show the global chart again.
# CLI
The portfolio holdings can be retrieved with the `holdings` command.
### Default holdings table view
```bash
$ cointop holdings
name symbol price holdings balance 24h% %holdings
Bitcoin BTC 11833.16 10 118331.6 -1.02 74.14
Ethereum ETH 394.9 100 39490 0.02 24.74
Dogecoin DOGE 0.00355861 500000 1779.3 1.46 1.11
```
### Output as csv
```bash
$ cointop holdings --format csv
name,symbol,price,holdings,balance,24h%,%holdings
Bitcoin,BTC,11833.16,10,118331.6,-1.02,74.16
Ethereum,ETH,394.48,100,39448,-0.18,24.72
Dogecoin,DOGE,0.00355861,500000,1779.3,1.46,1.12
```
### Output as json
```bash
$ cointop holdings --format json
[{"%holdings":"74.16","24h%":"-1.02","balance":"118331.6","holdings":"10","name":"Bitcoin","price":"11833.16","symbol":"BTC"},{"%holdings":"24.72","24h%":"-0.18","balance":"39448","holdings":"100","name":"Ethereum","price":"394.48","symbol":"ETH"},{"%holdings":"1.12","24h%":"1.46","balance":"1779.3","holdings":"500000","name":"Dogecoin","price":"0.00355861","symbol":"DOGE"}]
```
### Human readable numbers
Adds comma and dollar signs:
```bash
$ cointop holdings -h
name symbol price holdings balance 24h% %holdings
Bitcoin BTC $11,833.16 10 $118,331.6 -1.02% 74.14%
Ethereum ETH $394.9 100 $39,490 0.02% 24.74%
Dogecoin DOGE $0.00355861 500,000 $1,779.3 1.46% 1.11%
```
### Filter coins based on name or symbol
```bash
$ cointop holdings --filter btc,eth
name symbol price holdings balance 24h% %holdings
Bitcoin BTC 11833.16 10 118331.6 -1.02 74.16
Ethereum ETH 394.48 100 39448 -0.18 24.72
```
### Filter columns
```bash
$ cointop holdings --cols symbol,holdings,balance
symbol holdings balance
BTC 10 118331.6
ETH 100 39490
DOGE 500000 1779.3
```
### Output without headers
```bash
$ cointop holdings --no-header
Bitcoin BTC $11,833.16 10 $118,331.6 -1.02% 74.14%
Ethereum ETH $394.9 100 $39,490 0.02% 24.74%
Dogecoin DOGE $0.00355861 500,000 $1,779.3 1.46% 1.11%
```
### Convert to a different fiat currency
```bash
$ cointop holdings -h --convert eur
name symbol price holdings balance 24h% %holdings
Ethereum ETH €278.49 100 €27,849 -15.87% 100.00%
```
### Total portfolio value
```bash
$ cointop holdings --total
3671.32
```
### Combining flags
```bash
$ cointop holdings --total --filter btc,doge --format json -h
{"total":"$120,298.37"}
```
### Help
For all other options, see help command:
```bash
$ cointop holdings --help
```

@ -3,8 +3,9 @@
<li><a href="/install">Install</a></li>
<li><a href="/update">Update</a></li>
<li><a href="/getting-started">Getting started</a></li>
<li><a href="/shortcuts">Shortcuts</a></li>
<li><a href="/portfolio">Portfolio</a></li>
<li><a href="/colorschemes">Colorschemes</a></li>
<li><a href="/shortcuts">Shortcuts</a></li>
<li><a href="/config">Config</a></li>
<li><a href="/ssh">SSH</a></li>
<li><a href="/faq">FAQ</a></li>

@ -21,12 +21,14 @@ var DefaultCacheDir = "/tmp"
// FileCache ...
type FileCache struct {
muts map[string]*sync.Mutex
prefix string
cacheDir string
}
// Config ...
type Config struct {
CacheDir string
Prefix string
}
// NewFileCache ...
@ -39,7 +41,6 @@ func NewFileCache(config *Config) (*FileCache, error) {
if config.CacheDir != "" {
cacheDir = config.CacheDir
}
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
if err := os.MkdirAll(cacheDir, 0700); err != nil {
return nil, err
@ -49,6 +50,7 @@ func NewFileCache(config *Config) (*FileCache, error) {
return &FileCache{
muts: make(map[string]*sync.Mutex),
cacheDir: cacheDir,
prefix: config.Prefix,
}, nil
}
@ -62,7 +64,11 @@ func (f *FileCache) Set(key string, data interface{}, expire time.Duration) erro
defer f.muts[key].Unlock()
key = regexp.MustCompile("[^a-zA-Z0-9_-]").ReplaceAllLiteralString(key, "")
file := fmt.Sprintf("fcache.%s.%v", key, strconv.FormatInt(time.Now().Add(expire).Unix(), 10))
var prefix string
if f.prefix != "" {
prefix = fmt.Sprintf("%s.", f.prefix)
}
file := fmt.Sprintf("fcache.%s%s.%v", prefix, key, strconv.FormatInt(time.Now().Add(expire).Unix(), 10))
fpath := filepath.Join(f.cacheDir, file)
f.clean(key)
@ -91,7 +97,11 @@ func (f *FileCache) Set(key string, data interface{}, expire time.Duration) erro
// Get reads item from cache
func (f *FileCache) Get(key string, dst interface{}) error {
key = regexp.MustCompile("[^a-zA-Z0-9_-]").ReplaceAllLiteralString(key, "")
pattern := filepath.Join(f.cacheDir, fmt.Sprintf("fcache.%s.*", key))
var prefix string
if f.prefix != "" {
prefix = fmt.Sprintf("%s.", f.prefix)
}
pattern := filepath.Join(f.cacheDir, fmt.Sprintf("fcache.%s%s.*", prefix, key))
files, err := filepath.Glob(pattern)
if len(files) < 1 || err != nil {
return errors.New("fcache: no cache file found")

Loading…
Cancel
Save