portfolio chart

pull/22/head
Miguel Mota 6 years ago
parent e15da25bb7
commit 70e18090a9

@ -18,21 +18,32 @@ var chartpointslock sync.Mutex
func (ct *Cointop) updateChart() error {
chartlock.Lock()
defer chartlock.Unlock()
maxX := ct.maxtablewidth - 3
coin := ct.selectedCoinSymbol()
ct.chartPoints(maxX, coin)
if ct.portfoliovisible {
if err := ct.portfolioChart(); err != nil {
return err
}
} else {
coin := ct.selectedCoinSymbol()
ct.chartPoints(coin)
}
if len(ct.chartpoints) != 0 {
ct.chartview.Clear()
}
var body string
for i := range ct.chartpoints {
var s string
for j := range ct.chartpoints[i] {
p := ct.chartpoints[i][j]
s = fmt.Sprintf("%s%c", s, p.Ch)
}
body = fmt.Sprintf("%s%s\n", body, s)
if len(ct.chartpoints) == 0 {
body = "\n\n\n\n\nnot enough data for chart"
} else {
for i := range ct.chartpoints {
var s string
for j := range ct.chartpoints[i] {
p := ct.chartpoints[i][j]
s = fmt.Sprintf("%s%c", s, p.Ch)
}
body = fmt.Sprintf("%s%s\n", body, s)
}
}
ct.update(func() {
fmt.Fprint(ct.chartview, color.White(body))
@ -41,10 +52,11 @@ func (ct *Cointop) updateChart() error {
return nil
}
func (ct *Cointop) chartPoints(maxX int, coin string) error {
func (ct *Cointop) chartPoints(coin string) error {
maxX := ct.maxtablewidth - 3
chartpointslock.Lock()
defer chartpointslock.Unlock()
// TODO: not do this (SOC)
// TODO: not do this (SoC)
go ct.updateMarketbar()
chart := termui.NewLineChart()
@ -136,6 +148,114 @@ func (ct *Cointop) chartPoints(maxX int, coin string) error {
return nil
}
func (ct *Cointop) portfolioChart() error {
maxX := ct.maxtablewidth - 3
chartpointslock.Lock()
defer chartpointslock.Unlock()
// TODO: not do this (SoC)
go ct.updateMarketbar()
chart := termui.NewLineChart()
chart.Height = 10
chart.AxesColor = termui.ColorWhite
chart.LineColor = termui.ColorCyan
chart.Border = false
rangeseconds := ct.chartrangesmap[ct.selectedchartrange]
if ct.selectedchartrange == "YTD" {
ytd := time.Now().Unix() - int64(now.BeginningOfYear().Unix())
rangeseconds = time.Duration(ytd) * time.Second
}
now := time.Now()
nowseconds := now.Unix()
start := nowseconds - int64(rangeseconds.Seconds())
end := nowseconds
var data []float64
portfolio := ct.getPortfolioSlice()
chartname := ct.selectedCoinName()
for _, p := range portfolio {
// filter by selected chart if selected
if chartname != "" {
if chartname != p.Name {
continue
}
}
if p.Holdings <= 0 {
continue
}
var graphData []float64
coin := p.Symbol
cachekey := strings.ToLower(fmt.Sprintf("%s_%s", coin, strings.Replace(ct.selectedchartrange, " ", "", -1)))
cached, found := ct.cache.Get(cachekey)
if found {
// cache hit
graphData, _ = cached.([]float64)
ct.debuglog("soft cache hit")
} else {
_ = fcache.Get(cachekey, &graphData)
if len(graphData) == 0 {
time.Sleep(2 * time.Second)
apiGraphData, err := ct.api.GetCoinGraphData(coin, start, end)
if err != nil {
return err
}
for i := range apiGraphData.PriceUSD {
price := apiGraphData.PriceUSD[i][1]
graphData = append(graphData, price)
}
}
ct.cache.Set(cachekey, graphData, 10*time.Second)
go func() {
_ = fcache.Set(cachekey, graphData, 24*time.Hour)
}()
}
for i := range graphData {
price := graphData[i]
sum := p.Holdings * price
if len(data)-1 >= i {
data[i] += sum
}
data = append(data, sum)
}
}
chart.Data = data
termui.Body = termui.NewGrid()
termui.Body.Width = maxX
termui.Body.AddRows(
termui.NewRow(
termui.NewCol(12, 0, chart),
),
)
var points [][]termui.Cell
// calculate layout
termui.Body.Align()
w := termui.Body.Width
h := 10
row := termui.Body.Rows[0]
b := row.Buffer()
for i := 0; i < h; i = i + 1 {
var rowpoints []termui.Cell
for j := 0; j < w; j = j + 1 {
p := b.At(j, i)
rowpoints = append(rowpoints, p)
}
points = append(points, rowpoints)
}
ct.chartpoints = points
return nil
}
func (ct *Cointop) nextChartRange() error {
sel := 0
max := len(ct.chartranges)
@ -209,7 +329,8 @@ func (ct *Cointop) toggleCoinChart() error {
} else {
ct.selectedcoin = highlightedcoin
}
ct.updateChart()
ct.updateMarketbar()
go ct.updateChart()
go ct.updateMarketbar()
return nil
}

@ -201,3 +201,7 @@ func (ct *Cointop) setCurrencyConverstion(convert string) func() error {
return nil
}
}
func (ct *Cointop) currencySymbol() string {
return currencysymbols[ct.currencyconversion]
}

@ -47,8 +47,6 @@ func (ct *Cointop) updateHeaders() {
}
}
symbol := currencysymbols[ct.currencyconversion]
if ct.portfoliovisible {
cols = []string{"rank", "name", "symbol", "price",
"holdings", "balance", "24hchange", "lastupdated"}
@ -67,7 +65,7 @@ func (ct *Cointop) updateHeaders() {
var str string
d := s.arrow + s.displaytext
if v == "price" || v == "balance" {
d = s.arrow + symbol + s.displaytext
d = s.arrow + ct.currencySymbol() + s.displaytext
}
str = fmt.Sprintf(

@ -2,6 +2,7 @@ package cointop
import (
"fmt"
"math"
"time"
types "github.com/miguelmota/cointop/pkg/api/types"
@ -13,55 +14,81 @@ import (
func (ct *Cointop) updateMarketbar() error {
maxX := ct.width()
logo := fmt.Sprintf("%s%s%s%s", color.Green(""), color.Cyan(""), color.Green(""), color.Cyan("cointop"))
var content string
var market types.GlobalMarketData
var err error
cachekey := "market"
cached, found := ct.cache.Get(cachekey)
if found {
// cache hit
var ok bool
market, ok = cached.(types.GlobalMarketData)
if ok {
ct.debuglog("soft cache hit")
if ct.portfoliovisible {
total := ct.getPortfolioTotal()
if !(ct.currencyconversion == "BTC" || ct.currencyconversion == "ETH" || total < 1) {
total = math.Round(total*1e2) / 1e2
}
timeframe := ct.selectedchartrange
chartname := ct.selectedCoinName()
var charttitle string
if chartname == "" {
chartname = "Portfolio"
charttitle = color.Cyan(chartname)
} else {
charttitle = fmt.Sprintf("Portfolio - %s", color.Cyan(chartname))
}
content = fmt.Sprintf(
"[ Chart: %s %s ] Current Portfolio Value: %s%s",
charttitle,
timeframe,
ct.currencySymbol(),
humanize.Commaf(total),
)
} else {
market, err = ct.api.GetGlobalMarketData(ct.currencyconversion)
if err != nil {
return err
var market types.GlobalMarketData
var err error
cachekey := "market"
cached, found := ct.cache.Get(cachekey)
if found {
// cache hit
var ok bool
market, ok = cached.(types.GlobalMarketData)
if ok {
ct.debuglog("soft cache hit")
}
} else {
market, err = ct.api.GetGlobalMarketData(ct.currencyconversion)
if err != nil {
return err
}
ct.cache.Set(cachekey, market, 10*time.Second)
go func() {
_ = fcache.Set(cachekey, market, 24*time.Hour)
}()
}
ct.cache.Set(cachekey, market, 10*time.Second)
go func() {
_ = fcache.Set(cachekey, market, 24*time.Hour)
}()
}
timeframe := ct.selectedchartrange
chartname := ct.selectedCoinName()
if chartname == "" {
chartname = "Global"
}
timeframe := ct.selectedchartrange
chartname := ct.selectedCoinName()
if chartname == "" {
chartname = "Global"
content = fmt.Sprintf(
"[ Chart: %s %s ] Global ▶ Market Cap: $%s • 24H Volume: $%s • BTC Dominance: %.2f%% • Active Currencies: %s • Active Markets: %s",
color.Cyan(chartname),
timeframe,
humanize.Commaf(market.TotalMarketCapUSD),
humanize.Commaf(market.Total24HVolumeUSD),
market.BitcoinPercentageOfMarketCap,
humanize.Commaf(float64(market.ActiveCurrencies)),
humanize.Commaf(float64(market.ActiveMarkets)),
)
}
content = fmt.Sprintf("%s %s", logo, content)
content = pad.Right(content, maxX, " ")
ct.update(func() {
ct.marketbarview.Clear()
fmt.Fprintln(
ct.marketbarview,
pad.Right(
fmt.Sprintf(
"%s [ Chart: %s %s ] Global ▶ Market Cap: $%s • 24H Volume: $%s • BTC Dominance: %.2f%% • Active Currencies: %s • Active Markets: %s",
fmt.Sprintf("%s%s%s%s", color.Green(""), color.Cyan(""), color.Green(""), color.Cyan("cointop")),
color.Cyan(chartname),
timeframe,
humanize.Commaf(market.TotalMarketCapUSD),
humanize.Commaf(market.Total24HVolumeUSD),
market.BitcoinPercentageOfMarketCap,
humanize.Commaf(float64(market.ActiveCurrencies)),
humanize.Commaf(float64(market.ActiveMarkets)),
),
maxX,
" ",
),
)
fmt.Fprintln(ct.marketbarview, content)
})
return nil
}

@ -3,6 +3,7 @@ package cointop
import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
@ -47,7 +48,7 @@ func (ct *Cointop) updatePortfolioUpdateMenu() {
mode = "Add"
submitText = "Add"
}
header := color.GreenBg(fmt.Sprintf(" %s Portfolio Entry %s\n\n", mode, pad.Left("[q] close ", ct.maxtablewidth-15, " ")))
header := color.GreenBg(fmt.Sprintf(" %s Portfolio Entry %s\n\n", mode, pad.Left("[q] close ", ct.maxtablewidth-26, " ")))
label := fmt.Sprintf(" Enter holdings for %s %s", color.Yellow(coin.Name), current)
content := fmt.Sprintf("%s\n%s\n\n%s%s\n\n\n [Enter] %s [ESC] Cancel", header, label, strings.Repeat(" ", 29), coin.Symbol, submitText)
@ -170,6 +171,48 @@ func (ct *Cointop) portfolioEntriesCount() int {
return len(ct.portfolio.Entries)
}
func (ct *Cointop) getPortfolioSlice() []*coin {
sliced := []*coin{}
for i := range ct.allcoins {
if ct.portfolioEntriesCount() == 0 {
break
}
coin := ct.allcoins[i]
p, isNew := ct.portfolioEntry(coin)
if isNew {
continue
}
coin.Holdings = p.Holdings
balance := coin.Price * p.Holdings
balancestr := fmt.Sprintf("%.2f", balance)
if ct.currencyconversion == "ETH" || ct.currencyconversion == "BTC" {
balancestr = fmt.Sprintf("%.5f", balance)
}
balance, _ = strconv.ParseFloat(balancestr, 64)
coin.Balance = balance
sliced = append(sliced, coin)
}
sort.Slice(sliced, func(i, j int) bool {
return sliced[i].Balance > sliced[j].Balance
})
for i, coin := range sliced {
coin.Rank = i + 1
}
return sliced
}
func (ct *Cointop) getPortfolioTotal() float64 {
portfolio := ct.getPortfolioSlice()
var total float64
for _, p := range portfolio {
total += p.Balance
}
return total
}
func normalizeFloatstring(input string) string {
re := regexp.MustCompile(`(\d+\.\d+|\.\d+|\d+)`)
result := re.FindStringSubmatch(input)

@ -18,8 +18,7 @@ func (ct *Cointop) refreshAll() error {
ct.cache.Delete("market")
ct.updateCoins()
ct.updateTable()
ct.updateMarketbar()
ct.updateChart()
go ct.updateChart()
return nil
}

@ -7,11 +7,30 @@ import (
)
func (ct *Cointop) updateStatusbar(s string) {
currpage := ct.currentDisplayPage()
totalpages := ct.totalPages()
var quitText string
var favoritesText string
var portfolioText string
if ct.portfoliovisible || ct.filterByFavorites {
quitText = "Return"
} else {
quitText = "Quit"
}
if ct.portfoliovisible {
portfolioText = "[E]Edit"
} else {
portfolioText = "[P]Portfolio"
}
if ct.filterByFavorites {
favoritesText = "[Space]Unfavorite"
} else {
favoritesText = "[F]Favorites"
}
ct.update(func() {
ct.statusbarview.Clear()
currpage := ct.currentDisplayPage()
totalpages := ct.totalPages()
base := fmt.Sprintf("%sQuit %sHelp %sChart %sRange %sSearch %sConvert %sFavorites %sPortfolio %sSave", "[Q]", "[?]", "[Enter]", "[[ ]]", "[/]", "[C]", "[F]", "[P]", "[CTRL-S]")
base := fmt.Sprintf("%s%s %sHelp %sChart %sRange %sSearch %sConvert %s %s %sSave", "[Q]", quitText, "[?]", "[Enter]", "[[ ]]", "[/]", "[C]", favoritesText, portfolioText, "[CTRL-S]")
str := pad.Right(fmt.Sprintf("%v %sPage %v/%v %s", base, "[← →]", currpage, totalpages, s), ct.maxtablewidth, " ")
v := fmt.Sprintf("v%s", ct.version())
str = str[:len(str)-len(v)+2] + v

@ -3,7 +3,6 @@ package cointop
import (
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"
@ -140,6 +139,7 @@ func (ct *Cointop) refreshTable() error {
ct.tableview.Clear()
ct.table.Format().Fprint(ct.tableview)
ct.rowChanged()
go ct.updateChart()
})
return nil
@ -169,35 +169,7 @@ func (ct *Cointop) updateTable() error {
}
if ct.portfoliovisible {
for i := range ct.allcoins {
if ct.portfolioEntriesCount() == 0 {
break
}
coin := ct.allcoins[i]
p, isNew := ct.portfolioEntry(coin)
if isNew {
continue
}
coin.Holdings = p.Holdings
balance := coin.Price * p.Holdings
balancestr := fmt.Sprintf("%.2f", balance)
if ct.currencyconversion == "ETH" || ct.currencyconversion == "BTC" {
balancestr = fmt.Sprintf("%.5f", balance)
}
balance, _ = strconv.ParseFloat(balancestr, 64)
coin.Balance = balance
sliced = append(sliced, coin)
}
sort.Slice(sliced, func(i, j int) bool {
return sliced[i].Balance > sliced[j].Balance
})
for i, coin := range sliced {
coin.Rank = i + 1
}
sliced = ct.getPortfolioSlice()
ct.coins = sliced
ct.sort(ct.sortby, ct.sortdesc, ct.coins)
ct.refreshTable()

Loading…
Cancel
Save