Simon Roberts 3 years ago
parent aba283443d
commit f38bc4ca3f
No known key found for this signature in database
GPG Key ID: 0F30F99E6B771FD4

@ -2,17 +2,25 @@ package cointop
import ( import (
"fmt" "fmt"
"math"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/miguelmota/cointop/pkg/chartplot" "github.com/miguelmota/cointop/pkg/chartplot"
"github.com/miguelmota/cointop/pkg/timedata"
"github.com/miguelmota/cointop/pkg/timeutil" "github.com/miguelmota/cointop/pkg/timeutil"
"github.com/miguelmota/cointop/pkg/ui" "github.com/miguelmota/cointop/pkg/ui"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// Time-series data for a Coin used when building a Portfolio view for chart
type PriceData struct {
coin *Coin
data [][]float64
}
// ChartView is structure for chart view // ChartView is structure for chart view
type ChartView = ui.View type ChartView = ui.View
@ -121,7 +129,7 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
start := nowseconds - int64(rangeseconds.Seconds()) start := nowseconds - int64(rangeseconds.Seconds())
end := nowseconds end := nowseconds
var data []float64 var cacheData [][]float64
keyname := symbol keyname := symbol
if keyname == "" { if keyname == "" {
@ -132,21 +140,18 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
cached, found := ct.cache.Get(cachekey) cached, found := ct.cache.Get(cachekey)
if found { if found {
// cache hit // cache hit
data, _ = cached.([]float64) cacheData, _ = cached.([][]float64)
log.Debug("ChartPoints() soft cache hit") log.Debug("ChartPoints() soft cache hit")
} }
if len(data) == 0 { if len(cacheData) == 0 {
if symbol == "" { if symbol == "" {
convert := ct.State.currencyConversion convert := ct.State.currencyConversion
graphData, err := ct.api.GetGlobalMarketGraphData(convert, start, end) graphData, err := ct.api.GetGlobalMarketGraphData(convert, start, end)
if err != nil { if err != nil {
return nil return nil
} }
for i := range graphData.MarketCapByAvailableSupply { cacheData = graphData.MarketCapByAvailableSupply
price := graphData.MarketCapByAvailableSupply[i][1]
data = append(data, price)
}
} else { } else {
convert := ct.State.currencyConversion convert := ct.State.currencyConversion
graphData, err := ct.api.GetCoinGraphData(convert, symbol, name, start, end) graphData, err := ct.api.GetCoinGraphData(convert, symbol, name, start, end)
@ -157,20 +162,37 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
sort.Slice(sorted[:], func(i, j int) bool { sort.Slice(sorted[:], func(i, j int) bool {
return sorted[i][0] < sorted[j][0] return sorted[i][0] < sorted[j][0]
}) })
for i := range sorted { cacheData = sorted
price := sorted[i][1]
data = append(data, price)
}
} }
ct.cache.Set(cachekey, data, 10*time.Second) ct.cache.Set(cachekey, cacheData, 10*time.Second)
if ct.filecache != nil { if ct.filecache != nil {
go func() { go func() {
ct.filecache.Set(cachekey, data, 24*time.Hour) ct.filecache.Set(cachekey, cacheData, 24*time.Hour)
}() }()
} }
} }
// Resample cachedata
maxPoints := len(cacheData)
if maxPoints > 2*maxX {
maxPoints = 2 * maxX
}
timeQuantum := timedata.CalculateTimeQuantum(cacheData)
newStart := time.Unix(start, 0).Add(timeQuantum)
newEnd := time.Unix(end, 0).Add(-timeQuantum)
timeData := timedata.ResampleTimeSeriesData(cacheData, float64(newStart.UnixMilli()), float64(newEnd.UnixMilli()), maxPoints)
// Extract just the values from the data
var data []float64
for i := range timeData {
value := timeData[i][1]
if math.IsNaN(value) {
value = 0.0
}
data = append(data, value)
}
chart.SetData(data) chart.SetData(data)
ct.State.chartPoints = chart.GetChartPoints(maxX) ct.State.chartPoints = chart.GetChartPoints(maxX)
@ -203,7 +225,7 @@ func (ct *Cointop) PortfolioChart() error {
start := nowseconds - int64(rangeseconds.Seconds()) start := nowseconds - int64(rangeseconds.Seconds())
end := nowseconds end := nowseconds
var data []float64 var allCacheData []PriceData
portfolio := ct.GetPortfolioSlice() portfolio := ct.GetPortfolioSlice()
chartname := ct.SelectedCoinName() chartname := ct.SelectedCoinName()
for _, p := range portfolio { for _, p := range portfolio {
@ -218,46 +240,76 @@ func (ct *Cointop) PortfolioChart() error {
continue continue
} }
var graphData []float64 var cacheData [][]float64 // [][time,value]
cachekey := strings.ToLower(fmt.Sprintf("%s_%s_%s", p.Symbol, convert, 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) cached, found := ct.cache.Get(cachekey)
if found { if found {
// cache hit // cache hit
graphData, _ = cached.([]float64) cacheData, _ = cached.([][]float64)
log.Debug("PortfolioChart() soft cache hit") log.Debug("PortfolioChart() soft cache hit")
} else { } else {
if ct.filecache != nil { if ct.filecache != nil {
ct.filecache.Get(cachekey, &graphData) ct.filecache.Get(cachekey, &cacheData)
} }
if len(graphData) == 0 { if len(cacheData) == 0 {
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
apiGraphData, err := ct.api.GetCoinGraphData(convert, p.Symbol, p.Name, start, end) apiGraphData, err := ct.api.GetCoinGraphData(convert, p.Symbol, p.Name, start, end)
if err != nil { if err != nil {
return err return err
} }
sorted := apiGraphData.Price
sort.Slice(sorted[:], func(i, j int) bool { cacheData = apiGraphData.Price
return sorted[i][0] < sorted[j][0] sort.Slice(cacheData[:], func(i, j int) bool {
return cacheData[i][0] < cacheData[j][0]
}) })
for i := range sorted {
price := sorted[i][1]
graphData = append(graphData, price)
}
} }
ct.cache.Set(cachekey, graphData, 10*time.Second) ct.cache.Set(cachekey, cacheData, 10*time.Second)
if ct.filecache != nil { if ct.filecache != nil {
go func() { go func() {
ct.filecache.Set(cachekey, graphData, 24*time.Hour) ct.filecache.Set(cachekey, cacheData, 24*time.Hour)
}() }()
} }
} }
for i := range graphData { allCacheData = append(allCacheData, PriceData{p, cacheData})
price := graphData[i] }
sum := p.Holdings * price
// Calculate how many data points to provide to the chart. Limit maxPoints to 2*maxX
maxPoints := 0
for _, cacheData := range allCacheData {
if len(cacheData.data) > maxPoints {
maxPoints = len(cacheData.data)
}
}
if maxPoints > 2*maxX {
maxPoints = 2 * maxX
}
// Use the gap between price samples to adjust start/end in by one interval
var timeQuantum time.Duration
for _, cacheData := range allCacheData {
timeQuantum = timedata.CalculateTimeQuantum(cacheData.data)
if timeQuantum != 0 {
break // use the first one
}
}
newStart := time.Unix(start, 0).Add(timeQuantum)
newEnd := time.Unix(end, 0).Add(-timeQuantum)
// Resample and sum data
var data []float64
for _, cacheData := range allCacheData {
coinData := timedata.ResampleTimeSeriesData(cacheData.data, float64(newStart.UnixMilli()), float64(newEnd.UnixMilli()), maxPoints)
// sum (excluding NaN)
for i := range coinData {
price := coinData[i][1]
if math.IsNaN(price) {
price = 0.0
}
sum := cacheData.coin.Holdings * price
if i < len(data) { if i < len(data) {
data[i] += sum data[i] += sum
} else { } else {

@ -0,0 +1,51 @@
package timedata
import (
"math"
"sort"
"time"
log "github.com/sirupsen/logrus"
)
// Resample the [timestamp,value] data given to numsteps between start-end (returns numSteps+1 points).
// If the data does not extend past start/end then there will likely be NaN in the output data.
func ResampleTimeSeriesData(data [][]float64, start float64, end float64, numSteps int) [][]float64 {
var newData [][]float64
l := len(data)
step := (end - start) / float64(numSteps)
for pos := start; pos <= end; pos += step {
idx := sort.Search(l, func(i int) bool { return data[i][0] >= pos })
var val float64
if idx == 0 {
val = math.NaN() // off the left
} else if idx == l {
val = math.NaN() // off the right
} else {
// between two points - linear interpolation
left := data[idx-1]
right := data[idx]
dvdt := (right[1] - left[1]) / (right[0] - left[0])
val = left[1] + (pos-left[0])*dvdt
}
newData = append(newData, []float64{pos, val})
}
return newData
}
// Assuming that the [timestamp,value] data provided is roughly evenly spaced, calculate that interval.
func CalculateTimeQuantum(data [][]float64) time.Duration {
if len(data) > 1 {
minTime := time.UnixMilli(int64(data[0][0]))
maxTime := time.UnixMilli(int64(data[len(data)-1][0]))
return time.Duration(int64(maxTime.Sub(minTime)) / int64(len(data)-1))
}
return 0
}
// Print out all the [timestamp,value] data provided
func DebugLogPriceData(data [][]float64) {
for i := range data {
log.Debugf("%s %.2f", time.Unix(int64(data[i][0]/1000), 0), data[i][1])
}
}
Loading…
Cancel
Save