Skip to content
This repository was archived by the owner on Dec 16, 2025. It is now read-only.

Commit c0c514b

Browse files
authored
Merge pull request #180 from lyricnz/feature/resample-data
Resample portfolio data to consistent time-interval before charting
2 parents d4b6afa + e8fcd4a commit c0c514b

3 files changed

Lines changed: 125 additions & 55 deletions

File tree

cointop/chart.go

Lines changed: 67 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,24 @@ package cointop
22

33
import (
44
"fmt"
5+
"math"
56
"sort"
67
"sync"
78
"time"
89

910
"github.com/miguelmota/cointop/pkg/chartplot"
11+
"github.com/miguelmota/cointop/pkg/timedata"
1012
"github.com/miguelmota/cointop/pkg/timeutil"
1113
"github.com/miguelmota/cointop/pkg/ui"
1214
log "github.com/sirupsen/logrus"
1315
)
1416

17+
// PriceData is the time-series data for a Coin used when building a Portfolio view for chart
18+
type PriceData struct {
19+
coin *Coin
20+
data [][]float64
21+
}
22+
1523
// ChartView is structure for chart view
1624
type ChartView = ui.View
1725

@@ -120,7 +128,7 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
120128
start := nowseconds - int64(rangeseconds.Seconds())
121129
end := nowseconds
122130

123-
var data []float64
131+
var cacheData [][]float64
124132

125133
keyname := symbol
126134
if keyname == "" {
@@ -131,21 +139,18 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
131139
cached, found := ct.cache.Get(cachekey)
132140
if found {
133141
// cache hit
134-
data, _ = cached.([]float64)
142+
cacheData, _ = cached.([][]float64)
135143
log.Debug("ChartPoints() soft cache hit")
136144
}
137145

138-
if len(data) == 0 {
146+
if len(cacheData) == 0 {
139147
if symbol == "" {
140148
convert := ct.State.currencyConversion
141149
graphData, err := ct.api.GetGlobalMarketGraphData(convert, start, end)
142150
if err != nil {
143151
return nil
144152
}
145-
for i := range graphData.MarketCapByAvailableSupply {
146-
price := graphData.MarketCapByAvailableSupply[i][1]
147-
data = append(data, price)
148-
}
153+
cacheData = graphData.MarketCapByAvailableSupply
149154
} else {
150155
convert := ct.State.currencyConversion
151156
graphData, err := ct.api.GetCoinGraphData(convert, symbol, name, start, end)
@@ -156,20 +161,33 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
156161
sort.Slice(sorted[:], func(i, j int) bool {
157162
return sorted[i][0] < sorted[j][0]
158163
})
159-
for i := range sorted {
160-
price := sorted[i][1]
161-
data = append(data, price)
162-
}
164+
cacheData = sorted
163165
}
164166

165-
ct.cache.Set(cachekey, data, 10*time.Second)
167+
ct.cache.Set(cachekey, cacheData, 10*time.Second)
166168
if ct.filecache != nil {
167169
go func() {
168-
ct.filecache.Set(cachekey, data, 24*time.Hour)
170+
ct.filecache.Set(cachekey, cacheData, 24*time.Hour)
169171
}()
170172
}
171173
}
172174

175+
// Resample cachedata
176+
timeQuantum := timedata.CalculateTimeQuantum(cacheData)
177+
newStart := time.Unix(start, 0).Add(timeQuantum)
178+
newEnd := time.Unix(end, 0).Add(-timeQuantum)
179+
timeData := timedata.ResampleTimeSeriesData(cacheData, float64(newStart.UnixMilli()), float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX))
180+
181+
// Extract just the values from the data
182+
var data []float64
183+
for i := range timeData {
184+
value := timeData[i][1]
185+
if math.IsNaN(value) {
186+
value = 0.0
187+
}
188+
data = append(data, value)
189+
}
190+
173191
chart.SetData(data)
174192
ct.State.chartPoints = chart.GetChartPoints(maxX)
175193

@@ -202,7 +220,7 @@ func (ct *Cointop) PortfolioChart() error {
202220
start := nowseconds - int64(rangeseconds.Seconds())
203221
end := nowseconds
204222

205-
var data []float64
223+
var allCacheData []PriceData
206224
portfolio := ct.GetPortfolioSlice()
207225
chartname := ct.SelectedCoinName()
208226
for _, p := range portfolio {
@@ -217,46 +235,65 @@ func (ct *Cointop) PortfolioChart() error {
217235
continue
218236
}
219237

220-
var graphData []float64
238+
var cacheData [][]float64 // [][time,value]
221239
cachekey := ct.CompositeCacheKey(p.Symbol, p.Name, convert, selectedChartRange)
222240
cached, found := ct.cache.Get(cachekey)
223241
if found {
224242
// cache hit
225-
graphData, _ = cached.([]float64)
243+
cacheData, _ = cached.([][]float64)
226244
log.Debug("PortfolioChart() soft cache hit")
227245
} else {
228246
if ct.filecache != nil {
229-
ct.filecache.Get(cachekey, &graphData)
247+
ct.filecache.Get(cachekey, &cacheData)
230248
}
231249

232-
if len(graphData) == 0 {
250+
if len(cacheData) == 0 {
233251
time.Sleep(2 * time.Second)
234252

235253
apiGraphData, err := ct.api.GetCoinGraphData(convert, p.Symbol, p.Name, start, end)
236254
if err != nil {
237255
return err
238256
}
239-
sorted := apiGraphData.Price
240-
sort.Slice(sorted[:], func(i, j int) bool {
241-
return sorted[i][0] < sorted[j][0]
257+
258+
cacheData = apiGraphData.Price
259+
sort.Slice(cacheData[:], func(i, j int) bool {
260+
return cacheData[i][0] < cacheData[j][0]
242261
})
243-
for i := range sorted {
244-
price := sorted[i][1]
245-
graphData = append(graphData, price)
246-
}
247262
}
248263

249-
ct.cache.Set(cachekey, graphData, 10*time.Second)
264+
ct.cache.Set(cachekey, cacheData, 10*time.Second)
250265
if ct.filecache != nil {
251266
go func() {
252-
ct.filecache.Set(cachekey, graphData, 24*time.Hour)
267+
ct.filecache.Set(cachekey, cacheData, 24*time.Hour)
253268
}()
254269
}
255270
}
256271

257-
for i := range graphData {
258-
price := graphData[i]
259-
sum := p.Holdings * price
272+
allCacheData = append(allCacheData, PriceData{p, cacheData})
273+
}
274+
275+
// Use the gap between price samples to adjust start/end in by one interval
276+
var timeQuantum time.Duration
277+
for _, cacheData := range allCacheData {
278+
timeQuantum = timedata.CalculateTimeQuantum(cacheData.data)
279+
if timeQuantum != 0 {
280+
break // use the first one
281+
}
282+
}
283+
newStart := time.Unix(start, 0).Add(timeQuantum)
284+
newEnd := time.Unix(end, 0).Add(-timeQuantum)
285+
286+
// Resample and sum data
287+
var data []float64
288+
for _, cacheData := range allCacheData {
289+
coinData := timedata.ResampleTimeSeriesData(cacheData.data, float64(newStart.UnixMilli()), float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX))
290+
// sum (excluding NaN)
291+
for i := range coinData {
292+
price := coinData[i][1]
293+
if math.IsNaN(price) {
294+
price = 0.0
295+
}
296+
sum := cacheData.coin.Holdings * price
260297
if i < len(data) {
261298
data[i] += sum
262299
} else {

pkg/chartplot/chartplot.go

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package chartplot
22

33
import (
4-
"math"
5-
64
"github.com/miguelmota/cointop/pkg/termui"
75
)
86

@@ -53,13 +51,18 @@ func (c *ChartPlot) SetBorder(enabled bool) {
5351
func (c *ChartPlot) SetData(data []float64) {
5452
// NOTE: edit `termui.LineChart.shortenFloatVal(float64)` to not
5553
// use exponential notation.
54+
// NOTE: data should be the correct width for rendering - see GetChartDataSize()
5655
c.t.Data = data
5756
}
5857

58+
// GetChartDataSize ...
59+
func (c *ChartPlot) GetChartDataSize(width int) int {
60+
axisYWidth := 30
61+
return (width * 2) - axisYWidth
62+
}
63+
5964
// GetChartPoints ...
6065
func (c *ChartPlot) GetChartPoints(width int) [][]rune {
61-
axisYWidth := 30
62-
c.t.Data = interpolateData(c.t.Data, (width*2)-axisYWidth)
6366
termui.Body = termui.NewGrid()
6467
termui.Body.Width = width
6568
termui.Body.AddRows(
@@ -86,24 +89,3 @@ func (c *ChartPlot) GetChartPoints(width int) [][]rune {
8689

8790
return points
8891
}
89-
90-
func interpolateData(data []float64, width int) []float64 {
91-
var res []float64
92-
if len(data) == 0 {
93-
return res
94-
}
95-
stepFactor := float64(len(data)-1) / float64(width-1)
96-
res = append(res, data[0])
97-
for i := 1; i < width-1; i++ {
98-
step := float64(i) * stepFactor
99-
before := math.Floor(step)
100-
after := math.Ceil(step)
101-
atPoint := step - before
102-
pointBefore := data[int(before)]
103-
pointAfter := data[int(after)]
104-
interpolated := pointBefore + (pointAfter-pointBefore)*atPoint
105-
res = append(res, interpolated)
106-
}
107-
res = append(res, data[len(data)-1])
108-
return res
109-
}

pkg/timedata/timedata.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package timedata
2+
3+
import (
4+
"math"
5+
"sort"
6+
"time"
7+
8+
log "github.com/sirupsen/logrus"
9+
)
10+
11+
// Resample the [timestamp,value] data given to numsteps between start-end (returns numSteps+1 points).
12+
// If the data does not extend past start/end then there will likely be NaN in the output data.
13+
func ResampleTimeSeriesData(data [][]float64, start float64, end float64, numSteps int) [][]float64 {
14+
var newData [][]float64
15+
l := len(data)
16+
step := (end - start) / float64(numSteps)
17+
for pos := start; pos <= end; pos += step {
18+
idx := sort.Search(l, func(i int) bool { return data[i][0] >= pos })
19+
var val float64
20+
if idx == 0 {
21+
val = math.NaN() // off the left
22+
} else if idx == l {
23+
val = math.NaN() // off the right
24+
} else {
25+
// between two points - linear interpolation
26+
left := data[idx-1]
27+
right := data[idx]
28+
dvdt := (right[1] - left[1]) / (right[0] - left[0])
29+
val = left[1] + (pos-left[0])*dvdt
30+
}
31+
newData = append(newData, []float64{pos, val})
32+
}
33+
return newData
34+
}
35+
36+
// Assuming that the [timestamp,value] data provided is roughly evenly spaced, calculate that interval.
37+
func CalculateTimeQuantum(data [][]float64) time.Duration {
38+
if len(data) > 1 {
39+
minTime := time.UnixMilli(int64(data[0][0]))
40+
maxTime := time.UnixMilli(int64(data[len(data)-1][0]))
41+
return time.Duration(int64(maxTime.Sub(minTime)) / int64(len(data)-1))
42+
}
43+
return 0
44+
}
45+
46+
// Print out all the [timestamp,value] data provided
47+
func DebugLogPriceData(data [][]float64) {
48+
for i := range data {
49+
log.Debugf("%s %.2f", time.Unix(int64(data[i][0]/1000), 0), data[i][1])
50+
}
51+
}

0 commit comments

Comments
 (0)