Add support for declaring a BuyPrice and BuyCurrency in portfolio.

eg: ["Algorand", "125.4", "0.8", "USD"]

Add optional (default off) columns to portfolio:
"buy_price", "buy_currency", "profit", "profit_percent"

TODO:
- currency conversion
- (maybe) merge price/currency into one column
- add to "cointop holdings" output
- column sorting shortcuts
pull/243/head
Simon Roberts 3 years ago
parent 30fa30c057
commit 27fcf09513
No known key found for this signature in database
GPG Key ID: 0F30F99E6B771FD4

@ -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

@ -125,8 +125,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

@ -227,9 +227,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 {
@ -594,7 +597,7 @@ func (ct *Cointop) loadPortfolioFromConfig() error {
if !ok {
continue
}
if len(tupleIfc) > 2 {
if len(tupleIfc) > 4 {
continue
}
name, ok := tupleIfc[0].(string)
@ -607,7 +610,27 @@ func (ct *Cointop) loadPortfolioFromConfig() error {
return nil
}
if err := ct.SetPortfolioEntry(name, holdings); err != nil {
buyPrice := 0.0
if len(tupleIfc) >= 3 {
parsePrice, err := ct.InterfaceToFloat64(tupleIfc[2])
if err == nil {
buyPrice = parsePrice
} else {
return nil
}
}
buyCurrency := ""
if len(tupleIfc) >= 4 {
parseCurrency, ok := tupleIfc[3].(string)
if ok {
buyCurrency = parseCurrency
} else {
return nil
}
}
if err := ct.SetPortfolioEntry(name, holdings, buyPrice, buyCurrency); err != nil {
return err
}
}
@ -620,7 +643,7 @@ 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
}
}

@ -35,6 +35,11 @@ var SupportedPortfolioTableHeaders = []string{
"1y_change",
"percent_holdings",
"last_updated",
"buy_price",
"buy_currency",
// "buy_cost" // holdings * buy_price * conversion??
"profit",
"profit_percent",
}
// DefaultPortfolioTableHeaders are the default portfolio table header columns
@ -301,6 +306,92 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
Color: ct.colorscheme.TableRow,
Text: lastUpdated,
})
case "buy_price":
// TODO: finish
text := ct.FormatPrice(coin.BuyPrice)
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
}
if coin.BuyPrice == 0.0 {
text = ""
}
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 "buy_currency":
// TODO: finish - merge with buy_price?
text := coin.BuyCurrency
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 "profit":
// TODO: finish
text := ""
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
// TODO: currency conversion
profit := coin.Holdings * (coin.Price - coin.BuyPrice)
text = ct.FormatPrice(profit)
// text := strconv.FormatFloat(coin.Holdings, 'f', -1, 64)
}
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 "profit_percent":
profitPercent := 0.0
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
// TODO: currency conversion
profitPercent = 100 * (coin.Price/coin.BuyPrice - 1)
}
// text = ct.FormatPrice(profit)
colorProfit := ct.colorscheme.TableColumnChange
if profitPercent > 0 {
colorProfit = ct.colorscheme.TableColumnChangeUp
}
if profitPercent < 0 {
colorProfit = ct.colorscheme.TableColumnChangeDown
}
text := fmt.Sprintf("%.2f%%", profitPercent)
if coin.BuyPrice == 0.0 {
text = ""
}
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 +547,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 +598,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)
@ -511,8 +606,10 @@ func (ct *Cointop) SetPortfolioEntry(coin string, holdings float64) error {
if isNew {
key := strings.ToLower(coin)
ct.State.portfolio.Entries[key] = &PortfolioEntry{
Coin: coin,
Holdings: holdings,
Coin: coin,
Holdings: holdings,
BuyPrice: buyPrice,
BuyCurrency: buyCurrency,
}
} else {
p.Holdings = holdings
@ -564,6 +661,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,6 +787,7 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
records := make([][]string, len(holdings))
symbol := ct.CurrencySymbol()
// TODO: buy_price, buy_currency, profit, profit_percent, etc
headers := []string{"name", "symbol", "price", "holdings", "balance", "24h%", "%holdings"}
if len(filterCols) > 0 {
for _, col := range filterCols {

@ -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 "buy_price":
return a.BuyPrice < b.BuyPrice
case "buy_currency":
return a.BuyCurrency < b.BuyCurrency
case "profit":
return (a.Price - a.BuyPrice) < (b.Price - b.BuyPrice)
case "profit_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",
},
"buy_price": {
Slug: "buy_price",
Label: "buy price",
PlainLabel: "buy price",
},
"buy_currency": { // TODO: merge with price?
Slug: "buy_curency",
Label: "bcur",
PlainLabel: "bcur",
},
"profit": {
Slug: "profit",
Label: "profit",
PlainLabel: "profit",
},
"profit_percent": {
Slug: "profit_percent",
Label: "profit%",
PlainLabel: "profit%",
},
}
// GetLabel fetch the label to use for the heading (depends on configuration)
@ -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":

Loading…
Cancel
Save