From 6e99e9100f6df692657d257af01fdfdb7f2ac595 Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Mon, 27 Sep 2021 08:21:24 +1000 Subject: [PATCH 1/4] Add time labels to X axis --- cointop/chart.go | 9 ++++++++- pkg/chartplot/chartplot.go | 5 +++++ pkg/timedata/timedata.go | 27 ++++++++++++++++++++++----- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/cointop/chart.go b/cointop/chart.go index a60ff8f..df44409 100644 --- a/cointop/chart.go +++ b/cointop/chart.go @@ -177,6 +177,7 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error { newStart := time.Unix(start, 0).Add(timeQuantum) newEnd := time.Unix(end, 0).Add(-timeQuantum) timeData := timedata.ResampleTimeSeriesData(cacheData, float64(newStart.UnixMilli()), float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX)) + labels := timedata.BuildTimeSeriesLabels(timeData) // Extract just the values from the data var data []float64 @@ -189,6 +190,7 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error { } chart.SetData(data) + chart.SetDataLabels(labels) ct.State.chartPoints = chart.GetChartPoints(maxX) return nil @@ -285,8 +287,12 @@ func (ct *Cointop) PortfolioChart() error { // Resample and sum data var data []float64 - for _, cacheData := range allCacheData { + var labels []string + for i, cacheData := range allCacheData { coinData := timedata.ResampleTimeSeriesData(cacheData.data, float64(newStart.UnixMilli()), float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX)) + if i == 0 { + labels = timedata.BuildTimeSeriesLabels(coinData) + } // sum (excluding NaN) for i := range coinData { price := coinData[i][1] @@ -313,6 +319,7 @@ func (ct *Cointop) PortfolioChart() error { } chart.SetData(data) + chart.SetDataLabels(labels) ct.State.chartPoints = chart.GetChartPoints(maxX) return nil diff --git a/pkg/chartplot/chartplot.go b/pkg/chartplot/chartplot.go index 24fb2fb..33b3884 100644 --- a/pkg/chartplot/chartplot.go +++ b/pkg/chartplot/chartplot.go @@ -55,6 +55,11 @@ func (c *ChartPlot) SetData(data []float64) { c.t.Data = data } +// SetDataLabels ... +func (c *ChartPlot) SetDataLabels(labels []string) { + c.t.DataLabels = labels +} + // GetChartDataSize ... func (c *ChartPlot) GetChartDataSize(width int) int { axisYWidth := 30 diff --git a/pkg/timedata/timedata.go b/pkg/timedata/timedata.go index c605fdc..1d8e75d 100644 --- a/pkg/timedata/timedata.go +++ b/pkg/timedata/timedata.go @@ -8,7 +8,7 @@ import ( log "github.com/sirupsen/logrus" ) -// Resample the [timestamp,value] data given to numsteps between start-end (returns numSteps+1 points). +// ResampleTimeSeriesData resamples the given [timestamp,value] data 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 @@ -33,7 +33,7 @@ func ResampleTimeSeriesData(data [][]float64, start float64, end float64, numSte return newData } -// Assuming that the [timestamp,value] data provided is roughly evenly spaced, calculate that interval. +// CalculateTimeQuantum determines the given [timestamp,value] data func CalculateTimeQuantum(data [][]float64) time.Duration { if len(data) > 1 { minTime := time.UnixMilli(int64(data[0][0])) @@ -43,9 +43,26 @@ func CalculateTimeQuantum(data [][]float64) time.Duration { return 0 } -// Print out all the [timestamp,value] data provided -func DebugLogPriceData(data [][]float64) { +// BuildTimeSeriesLabels returns a list of short labels representing time values from the given [timestamp,value] data +func BuildTimeSeriesLabels(data [][]float64) []string { + minTime := time.UnixMilli(int64(data[0][0])) + maxTime := time.UnixMilli(int64(data[len(data)-1][0])) + timeRange := maxTime.Sub(minTime) + log.Debugf("XXX BuildTimeSeriesLabels %s to %s = %s", minTime, maxTime, timeRange) + + var timeFormat string + if timeRange.Hours() < 24 { + timeFormat = "15:04" + } else if timeRange.Hours() < 24*7 { + timeFormat = "Mon 15:04" + } else if timeRange.Hours() < 24*365 { + timeFormat = "02-Jan" + } else { + timeFormat = "Jan 2006" + } + var labels []string for i := range data { - log.Debugf("%s %.2f", time.Unix(int64(data[i][0]/1000), 0), data[i][1]) + labels = append(labels, time.UnixMilli(int64(data[i][0])).Format(timeFormat)) } + return labels } From 620e5c737ca1c85da66c1f0a860e10a157598fc3 Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Mon, 27 Sep 2021 08:28:42 +1000 Subject: [PATCH 2/4] Remove debug code --- pkg/timedata/timedata.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/timedata/timedata.go b/pkg/timedata/timedata.go index 1d8e75d..8e25ded 100644 --- a/pkg/timedata/timedata.go +++ b/pkg/timedata/timedata.go @@ -4,8 +4,6 @@ import ( "math" "sort" "time" - - log "github.com/sirupsen/logrus" ) // ResampleTimeSeriesData resamples the given [timestamp,value] data to numsteps between start-end (returns numSteps+1 points). @@ -48,7 +46,6 @@ func BuildTimeSeriesLabels(data [][]float64) []string { minTime := time.UnixMilli(int64(data[0][0])) maxTime := time.UnixMilli(int64(data[len(data)-1][0])) timeRange := maxTime.Sub(minTime) - log.Debugf("XXX BuildTimeSeriesLabels %s to %s = %s", minTime, maxTime, timeRange) var timeFormat string if timeRange.Hours() < 24 { From 2f616e2c3520d1a5d98e379b2d8dfc0f5437a625 Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Mon, 27 Sep 2021 20:04:17 +1000 Subject: [PATCH 3/4] When we are not aggregating multiple prices, use the oldest data available as the left hand side of chart, not the requested start. --- cointop/chart.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cointop/chart.go b/cointop/chart.go index df44409..223fc6b 100644 --- a/cointop/chart.go +++ b/cointop/chart.go @@ -174,9 +174,9 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error { // Resample cachedata timeQuantum := timedata.CalculateTimeQuantum(cacheData) - newStart := time.Unix(start, 0).Add(timeQuantum) + newStart := cacheData[0][0] // use the first data point newEnd := time.Unix(end, 0).Add(-timeQuantum) - timeData := timedata.ResampleTimeSeriesData(cacheData, float64(newStart.UnixMilli()), float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX)) + timeData := timedata.ResampleTimeSeriesData(cacheData, newStart, float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX)) labels := timedata.BuildTimeSeriesLabels(timeData) // Extract just the values from the data From 5ef09ea40aab652feec6b67d5dddb7b5646f8435 Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Tue, 28 Sep 2021 08:00:38 +1000 Subject: [PATCH 4/4] Handle 0/1 data items returned before resampling --- cointop/chart.go | 83 ++++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/cointop/chart.go b/cointop/chart.go index 223fc6b..1c1e1bd 100644 --- a/cointop/chart.go +++ b/cointop/chart.go @@ -172,21 +172,24 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error { } } - // Resample cachedata - timeQuantum := timedata.CalculateTimeQuantum(cacheData) - newStart := cacheData[0][0] // use the first data point - newEnd := time.Unix(end, 0).Add(-timeQuantum) - timeData := timedata.ResampleTimeSeriesData(cacheData, newStart, float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX)) - labels := timedata.BuildTimeSeriesLabels(timeData) - - // Extract just the values from the data + var labels []string var data []float64 - for i := range timeData { - value := timeData[i][1] - if math.IsNaN(value) { - value = 0.0 + timeQuantum := timedata.CalculateTimeQuantum(cacheData) // will be 0 if <2 points + if timeQuantum > 0 { + // Resample cachedata + newStart := cacheData[0][0] // use the first data point + newEnd := time.Unix(end, 0).Add(-timeQuantum) + timeData := timedata.ResampleTimeSeriesData(cacheData, newStart, float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX)) + labels = timedata.BuildTimeSeriesLabels(timeData) + + // Extract just the values from the data + for i := range timeData { + value := timeData[i][1] + if math.IsNaN(value) { + value = 0.0 + } + data = append(data, value) } - data = append(data, value) } chart.SetData(data) @@ -282,38 +285,42 @@ func (ct *Cointop) PortfolioChart() error { break // use the first one } } - newStart := time.Unix(start, 0).Add(timeQuantum) - newEnd := time.Unix(end, 0).Add(-timeQuantum) - // Resample and sum data + // If there is data, resample and sum var data []float64 var labels []string - for i, cacheData := range allCacheData { - coinData := timedata.ResampleTimeSeriesData(cacheData.data, float64(newStart.UnixMilli()), float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX)) - if i == 0 { - labels = timedata.BuildTimeSeriesLabels(coinData) - } - // sum (excluding NaN) - for i := range coinData { - price := coinData[i][1] - if math.IsNaN(price) { - price = 0.0 + if timeQuantum > 0 { + newStart := time.Unix(start, 0).Add(timeQuantum) + newEnd := time.Unix(end, 0).Add(-timeQuantum) + + // Resample and sum data + for i, cacheData := range allCacheData { + coinData := timedata.ResampleTimeSeriesData(cacheData.data, float64(newStart.UnixMilli()), float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX)) + if i == 0 { + labels = timedata.BuildTimeSeriesLabels(coinData) } - sum := cacheData.coin.Holdings * price - if i < len(data) { - data[i] += sum - } else { - data = append(data, sum) + // 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 { + data = append(data, sum) + } } } - } - // Scale Portfolio Balances to hide value - if ct.State.hidePortfolioBalances { - var lastPrice = data[len(data)-1] - if lastPrice > 0.0 { - for i, price := range data { - data[i] = 100 * price / lastPrice + // Scale Portfolio Balances to hide value + if ct.State.hidePortfolioBalances { + var lastPrice = data[len(data)-1] + if lastPrice > 0.0 { + for i, price := range data { + data[i] = 100 * price / lastPrice + } } } }