diff --git a/candlestick_series.go b/candlestick_series.go index 76bb388..315ec6a 100644 --- a/candlestick_series.go +++ b/candlestick_series.go @@ -3,8 +3,13 @@ package chart import ( "fmt" "time" + + "math" + + "github.com/wcharczuk/go-chart/util" ) +// CandleValue is a day's data for a candlestick plot. type CandleValue struct { Timestamp time.Time High float64 @@ -13,6 +18,11 @@ type CandleValue struct { Close float64 } +// IsZero returns if the value is zero or not. +func (cv CandleValue) IsZero() bool { + return cv.Timestamp.IsZero() +} + // CandlestickSeries is a special type of series that takes a norma value provider // and maps it to day value stats (high, low, open, close). type CandlestickSeries struct { @@ -46,20 +56,52 @@ func (cs CandlestickSeries) CandleValues() []CandleValue { // compute the low, or the min totalValues := cs.InnerSeries.Len() + if totalValues == 0 { + return nil + } + var value CandleValue var values []CandleValue + var lastYear, lastMonth, lastDay int + var year, month, day int - var day int - for i := 0; i < totalValues; i++ { - if day == 0 { - // extract day value from time value + var tv float64 + var t time.Time + var lv, v float64 + + tv, v = cs.InnerSeries.GetValues(0) + t = util.Time.FromFloat64(tv) + year, month, day = t.Year(), int(t.Month()), t.Day() + value.Timestamp = cs.newTimestamp(year, month, day) + value.Open, value.Low, value.High = v, v, v + + for i := 1; i < totalValues; i++ { + tv, v = cs.InnerSeries.GetValues(i) + t = util.Time.FromFloat64(tv) + year, month, day = t.Year(), int(t.Month()), t.Day() + + // if we've transitioned to a new day or we're on the last value + if lastYear != year || lastMonth != month || lastDay != day || i == (totalValues-1) { + value.Close = lv + values = append(values, value) + + value = CandleValue{ + Timestamp: cs.newTimestamp(year, month, day), + } } - if + + value.Low = math.Min(value.Low, v) + value.High = math.Max(value.Low, v) + lv = v } return values } +func (cs CandlestickSeries) newTimestamp(year, month, day int) time.Time { + return time.Date(year, time.Month(month), day, 12, 0, 0, 0, util.Date.Eastern()) +} + // Render implements Series.Render. func (cs CandlestickSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { //style := cs.Style.InheritFrom(defaults) diff --git a/candlestick_series_test.go b/candlestick_series_test.go new file mode 100644 index 0000000..aeb8165 --- /dev/null +++ b/candlestick_series_test.go @@ -0,0 +1,45 @@ +package chart + +import ( + "math/rand" + "testing" + "time" + + assert "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/util" +) + +func generateDummyStockData() (times []time.Time, prices []float64) { + start := util.Date.On(time.Date(2017, 05, 15, 6, 30, 0, 0, util.Date.Eastern()), util.NYSEOpen()) + var cursor time.Time + for day := 0; day < 60; day++ { + cursor = start.AddDate(0, 0, day) + for hour := 0; hour < 7; hour++ { + for minute := 0; minute < 60; minute++ { + times = append(times, cursor) + prices = append(prices, rand.Float64()*256) + + cursor = cursor.Add(time.Minute) + } + + cursor = cursor.Add(time.Hour) + } + } + return +} + +func TestCandlestickSeriesCandleValues(t *testing.T) { + assert := assert.New(t) + + xdata, ydata := generateDummyStockData() + + candleSeries := CandlestickSeries{ + InnerSeries: TimeSeries{ + XValues: xdata, + YValues: ydata, + }, + } + + values := candleSeries.CandleValues() + assert.NotEmpty(values) +} diff --git a/market_hours_range.go b/market_hours_range.go index de32e8c..6bbf200 100644 --- a/market_hours_range.go +++ b/market_hours_range.go @@ -115,31 +115,31 @@ func (mhr MarketHoursRange) GetMarketClose() time.Time { // GetTicks returns the ticks for the range. // This is to override the default continous ticks that would be generated for the range. func (mhr *MarketHoursRange) GetTicks(r Renderer, defaults Style, vf ValueFormatter) []Tick { - times := seq.Time.MarketHours(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + times := seq.TimeUtil.MarketHours(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) timesWidth := mhr.measureTimes(r, defaults, vf, times) if timesWidth <= mhr.Domain { return mhr.makeTicks(vf, times) } - times = seq.Time.MarketHourQuarters(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + times = seq.TimeUtil.MarketHourQuarters(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) timesWidth = mhr.measureTimes(r, defaults, vf, times) if timesWidth <= mhr.Domain { return mhr.makeTicks(vf, times) } - times = seq.Time.MarketDayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + times = seq.TimeUtil.MarketDayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) timesWidth = mhr.measureTimes(r, defaults, vf, times) if timesWidth <= mhr.Domain { return mhr.makeTicks(vf, times) } - times = seq.Time.MarketDayAlternateCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + times = seq.TimeUtil.MarketDayAlternateCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) timesWidth = mhr.measureTimes(r, defaults, vf, times) if timesWidth <= mhr.Domain { return mhr.makeTicks(vf, times) } - times = seq.Time.MarketDayMondayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + times = seq.TimeUtil.MarketDayMondayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) timesWidth = mhr.measureTimes(r, defaults, vf, times) if timesWidth <= mhr.Domain { return mhr.makeTicks(vf, times)