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 (
"fmt"
"math"
"sort"
"strings"
"sync"
"time"
"github.com/miguelmota/cointop/pkg/chartplot"
"github.com/miguelmota/cointop/pkg/timedata"
"github.com/miguelmota/cointop/pkg/timeutil"
"github.com/miguelmota/cointop/pkg/ui"
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
type ChartView = ui.View
@ -121,7 +129,7 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
start := nowseconds - int64(rangeseconds.Seconds())
end := nowseconds
var data []float64
var cacheData [][]float64
keyname := symbol
if keyname == "" {
@ -132,21 +140,18 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
cached, found := ct.cache.Get(cachekey)
if found {
// cache hit
data, _ = cached.([]float64)
cacheData, _ = cached.([][]float64)
log.Debug("ChartPoints() soft cache hit")
}
if len(data) == 0 {
if len(cacheData) == 0 {
if symbol == "" {
convert := ct.State.currencyConversion
graphData, err := ct.api.GetGlobalMarketGraphData(convert, start, end)
if err != nil {
return nil
}
for i := range graphData.MarketCapByAvailableSupply {
price := graphData.MarketCapByAvailableSupply[i][1]
data = append(data, price)
}
cacheData = graphData.MarketCapByAvailableSupply
} else {
convert := ct.State.currencyConversion
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 {
return sorted[i][0] < sorted[j][0]
})
for i := range sorted {
price := sorted[i][1]
data = append(data, price)
}
cacheData = sorted
}
ct.cache.Set(cachekey, data, 10*time.Second)
ct.cache.Set(cachekey, cacheData, 10*time.Second)
if ct.filecache != nil {
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)
ct.State.chartPoints = chart.GetChartPoints(maxX)
@ -203,7 +225,7 @@ func (ct *Cointop) PortfolioChart() error {
start := nowseconds - int64(rangeseconds.Seconds())
end := nowseconds
var data []float64
var allCacheData []PriceData
portfolio := ct.GetPortfolioSlice()
chartname := ct.SelectedCoinName()
for _, p := range portfolio {
@ -218,46 +240,76 @@ func (ct *Cointop) PortfolioChart() error {
continue
}
var graphData []float64
var cacheData [][]float64 // [][time,value]
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
graphData, _ = cached.([]float64)
cacheData, _ = cached.([][]float64)
log.Debug("PortfolioChart() soft cache hit")
} else {
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)
apiGraphData, err := ct.api.GetCoinGraphData(convert, p.Symbol, p.Name, start, end)
if err != nil {
return err
}
sorted := apiGraphData.Price
sort.Slice(sorted[:], func(i, j int) bool {
return sorted[i][0] < sorted[j][0]
cacheData = apiGraphData.Price
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 {
go func() {
ct.filecache.Set(cachekey, graphData, 24*time.Hour)
ct.filecache.Set(cachekey, cacheData, 24*time.Hour)
}()
}
}
for i := range graphData {
price := graphData[i]
sum := p.Holdings * price
allCacheData = append(allCacheData, PriceData{p, cacheData})
}
// 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) {
data[i] += sum
} 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