diff --git a/_examples/candlestick_series/main.go b/_examples/candlestick_series/main.go index cf233e4..c143029 100644 --- a/_examples/candlestick_series/main.go +++ b/_examples/candlestick_series/main.go @@ -1,215 +1,36 @@ package main import ( + "math/rand" "net/http" - "strings" "time" chart "github.com/wcharczuk/go-chart" - "github.com/wcharczuk/go-chart/seq" util "github.com/wcharczuk/go-chart/util" ) -type price struct { - Timestamp string - Price float64 -} - -func (p price) format() (time.Time, float64) { - t, err := time.ParseInLocation("2006-01-02 15:04:05", strings.Split(p.Timestamp, ".")[0], util.Date.Eastern()) - if err != nil { - panic(err) - } - return t, p.Price -} - func stockData() (times []time.Time, prices []float64) { - rawPrices := []price{ - {"2017-05-12 19:45:04.482809", 238.84}, - {"2017-05-12 19:30:04.349476", 238.92}, - {"2017-05-12 19:15:04.160628", 238.98}, - {"2017-05-12 19:00:03.994135", 239.05}, - {"2017-05-12 18:45:03.947176", 238.87}, - {"2017-05-12 18:30:03.653826", 238.97}, - {"2017-05-12 18:15:03.319783", 239.00}, - {"2017-05-12 18:00:03.293044", 238.87}, - {"2017-05-12 17:45:03.010216", 238.85}, - {"2017-05-12 17:30:07.808406", 238.84}, - {"2017-05-12 17:15:02.814348", 238.93}, - {"2017-05-12 17:00:03.153611", 239.00}, - {"2017-05-12 16:45:02.352906", 238.93}, - {"2017-05-12 16:30:02.339523", 239.00}, - {"2017-05-12 16:15:02.051624", 239.05}, - {"2017-05-12 16:00:01.920557", 239.10}, - {"2017-05-12 15:45:06.866833", 239.00}, - {"2017-05-12 15:30:01.60765", 238.92}, - {"2017-05-12 15:15:01.481936", 238.80}, - {"2017-05-12 15:00:01.298738", 238.85}, - {"2017-05-12 14:45:01.19513", 238.78}, - {"2017-05-12 14:30:01.037173", 239.03}, - {"2017-05-12 14:15:00.835599", 239.02}, - {"2017-05-12 14:00:00.654748", 239.00}, - {"2017-05-12 13:45:00.65331", 239.05}, - {"2017-05-11 19:45:03.940287", 239.26}, - {"2017-05-11 19:30:03.796674", 239.42}, - {"2017-05-11 19:15:03.709477", 239.31}, - {"2017-05-11 19:00:03.533934", 239.37}, - {"2017-05-11 18:45:03.383436", 239.44}, - {"2017-05-11 18:30:03.224035", 239.23}, - {"2017-05-11 18:15:03.127204", 239.31}, - {"2017-05-11 18:00:02.8859", 239.27}, - {"2017-05-11 17:45:02.796172", 239.00}, - {"2017-05-11 17:30:02.767553", 239.12}, - {"2017-05-11 17:15:02.56807", 238.96}, - {"2017-05-11 17:00:02.613708", 239.08}, - {"2017-05-11 16:45:02.348852", 238.99}, - {"2017-05-11 16:30:02.165067", 238.84}, - {"2017-05-11 16:15:02.043199", 238.69}, - {"2017-05-11 16:00:01.831857", 238.67}, - {"2017-05-11 15:45:01.705654", 238.72}, - {"2017-05-11 15:30:01.641864", 238.75}, - {"2017-05-11 15:15:01.387109", 238.58}, - {"2017-05-11 15:00:01.212597", 238.53}, - {"2017-05-11 14:45:01.148888", 238.35}, - {"2017-05-11 14:30:00.915767", 238.59}, - {"2017-05-11 14:15:00.655649", 238.55}, - {"2017-05-11 14:00:00.596624", 239.05}, - {"2017-05-11 13:45:00.565487", 239.36}, - {"2017-05-10 19:45:04.220057", 239.69}, - {"2017-05-10 19:30:04.102519", 239.82}, - {"2017-05-10 19:15:03.950613", 239.77}, - {"2017-05-10 19:00:03.871972", 239.73}, - {"2017-05-10 18:45:03.697585", 239.59}, - {"2017-05-10 18:30:03.490385", 239.72}, - {"2017-05-10 18:15:03.327691", 239.71}, - {"2017-05-10 18:00:03.277211", 239.68}, - {"2017-05-10 17:45:03.270592", 239.76}, - {"2017-05-10 17:30:03.124085", 239.69}, - {"2017-05-10 17:15:02.811264", 239.76}, - {"2017-05-10 17:00:02.696942", 239.71}, - {"2017-05-10 16:45:02.435806", 239.66}, - {"2017-05-10 16:30:02.373372", 239.65}, - {"2017-05-10 16:15:02.130672", 239.66}, - {"2017-05-10 16:00:02.04879", 239.71}, - {"2017-05-10 15:45:01.91206", 239.74}, - {"2017-05-10 15:30:01.710121", 239.57}, - {"2017-05-10 15:15:01.485801", 239.39}, - {"2017-05-10 15:00:01.43788", 239.44}, - {"2017-05-10 14:45:01.184263", 239.25}, - {"2017-05-10 14:30:00.940762", 239.45}, - {"2017-05-10 14:15:00.885742", 239.41}, - {"2017-05-10 14:00:00.640709", 239.38}, - {"2017-05-10 13:45:00.548287", 239.38}, - {"2017-05-09 19:45:04.220057", 239.69}, - {"2017-05-09 19:30:04.102519", 239.82}, - {"2017-05-09 19:15:03.950613", 239.77}, - {"2017-05-09 19:00:03.871972", 239.73}, - {"2017-05-09 18:45:03.697585", 239.59}, - {"2017-05-09 18:30:03.490385", 239.72}, - {"2017-05-09 18:15:03.327691", 239.71}, - {"2017-05-09 18:00:03.277211", 239.68}, - {"2017-05-09 17:45:03.270592", 239.76}, - {"2017-05-09 17:30:03.124085", 239.69}, - {"2017-05-09 17:15:02.811264", 239.76}, - {"2017-05-09 17:00:02.696942", 239.71}, - {"2017-05-09 16:45:02.435806", 239.66}, - {"2017-05-09 16:30:02.373372", 239.65}, - {"2017-05-09 16:15:02.130672", 239.66}, - {"2017-05-09 16:00:02.04879", 239.71}, - {"2017-05-09 15:45:01.91206", 239.74}, - {"2017-05-09 15:30:01.710121", 239.57}, - {"2017-05-09 15:15:01.485801", 239.39}, - {"2017-05-09 15:00:01.43788", 239.44}, - {"2017-05-09 14:45:01.184263", 239.25}, - {"2017-05-09 14:30:00.940762", 239.45}, - {"2017-05-09 14:15:00.885742", 239.41}, - {"2017-05-09 14:00:00.640709", 239.38}, - {"2017-05-09 13:45:00.548287", 239.38}, - {"2017-05-05 19:45:04.220057", 239.69}, - {"2017-05-05 19:30:04.102519", 239.82}, - {"2017-05-05 19:15:03.950613", 239.77}, - {"2017-05-05 19:00:03.871972", 239.73}, - {"2017-05-05 18:45:03.697585", 239.59}, - {"2017-05-05 18:30:03.490385", 239.72}, - {"2017-05-05 18:15:03.327691", 239.71}, - {"2017-05-05 18:00:03.277211", 239.68}, - {"2017-05-05 17:45:03.270592", 239.76}, - {"2017-05-05 17:30:03.124085", 239.69}, - {"2017-05-05 17:15:02.811264", 239.76}, - {"2017-05-05 17:00:02.696942", 239.71}, - {"2017-05-05 16:45:02.435806", 239.66}, - {"2017-05-05 16:30:02.373372", 239.65}, - {"2017-05-05 16:15:02.130672", 239.66}, - {"2017-05-05 16:00:02.04879", 239.71}, - {"2017-05-05 15:45:01.91206", 239.74}, - {"2017-05-05 15:30:01.710121", 239.57}, - {"2017-05-05 15:15:01.485801", 239.39}, - {"2017-05-05 15:00:01.43788", 239.44}, - {"2017-05-05 14:45:01.184263", 239.25}, - {"2017-05-05 14:30:00.940762", 239.45}, - {"2017-05-05 14:15:00.885742", 239.41}, - {"2017-05-05 14:00:00.640709", 239.38}, - {"2017-05-05 13:45:00.548287", 239.38}, - {"2017-05-03 19:45:04.220057", 239.69}, - {"2017-05-03 19:30:04.102519", 239.82}, - {"2017-05-03 19:15:03.950613", 239.77}, - {"2017-05-03 19:00:03.871972", 239.73}, - {"2017-05-03 18:45:03.697585", 239.59}, - {"2017-05-03 18:30:03.490385", 239.72}, - {"2017-05-03 18:15:03.327691", 239.71}, - {"2017-05-03 18:00:03.277211", 239.68}, - {"2017-05-03 17:45:03.270592", 239.76}, - {"2017-05-03 17:30:03.124085", 239.69}, - {"2017-05-03 17:15:02.811264", 239.76}, - {"2017-05-03 17:00:02.696942", 239.71}, - {"2017-05-03 16:45:02.435806", 239.66}, - {"2017-05-03 16:30:02.373372", 239.65}, - {"2017-05-03 16:15:02.130672", 239.66}, - {"2017-05-03 16:00:02.04879", 239.71}, - {"2017-05-03 15:45:01.91206", 239.74}, - {"2017-05-03 15:30:01.710121", 239.57}, - {"2017-05-03 15:15:01.485801", 239.39}, - {"2017-05-03 15:00:01.43788", 239.44}, - {"2017-05-03 14:45:01.184263", 239.25}, - {"2017-05-03 14:30:00.940762", 239.45}, - {"2017-05-03 14:15:00.885742", 239.41}, - {"2017-05-03 14:00:00.640709", 239.38}, - {"2017-05-03 13:45:00.548287", 239.38}, - {"2017-04-28 19:45:04.193877", 238.11}, - {"2017-04-28 19:30:04.078242", 238.20}, - {"2017-04-28 19:15:03.886795", 238.05}, - {"2017-04-28 19:00:03.797682", 238.08}, - {"2017-04-28 18:45:03.691054", 238.05}, - {"2017-04-28 18:30:03.513045", 237.96}, - {"2017-04-28 18:15:03.387037", 238.09}, - {"2017-04-28 18:00:03.303554", 238.11}, - {"2017-04-28 17:45:03.181305", 238.16}, - {"2017-04-28 17:30:08.066927", 238.17}, - {"2017-04-28 17:15:02.708957", 238.23}, - {"2017-04-28 17:00:02.673222", 238.27}, - {"2017-04-28 16:45:02.478988", 238.29}, - {"2017-04-28 16:30:02.299229", 238.24}, - {"2017-04-28 16:15:02.100061", 238.15}, - {"2017-04-28 16:00:01.922355", 238.06}, - {"2017-04-28 15:45:01.666185", 238.25}, - {"2017-04-28 15:30:01.477215", 238.19}, - {"2017-04-28 15:15:01.440657", 238.39}, - {"2017-04-28 15:00:01.222734", 238.39}, - {"2017-04-28 14:45:01.005045", 238.33}, - {"2017-04-28 14:30:00.935222", 238.46}, - {"2017-04-28 14:15:00.84484", 238.53}, - {"2017-04-28 14:00:00.635436", 238.52}, - {"2017-04-27 19:45:04.242597", 238.61}, - } + start := time.Date(2017, 05, 15, 9, 30, 0, 0, util.Date.Eastern()) + price := 256.0 + for day := 0; day < 60; day++ { + cursor := start.AddDate(0, 0, day) - var t time.Time - var v float64 - var p price - for i := len(rawPrices) - 1; i >= 0; i-- { - p = rawPrices[i] - t, v = p.format() - times = append(times, t) - prices = append(prices, v) + if util.Date.IsNYSEHoliday(cursor) { + continue + } + + for minute := 0; minute < ((6 * 60) + 30); minute++ { + cursor = cursor.Add(time.Minute) + + if rand.Float64() >= 0.5 { + price = price + (rand.Float64() * (price * 0.01)) + } else { + price = price - (rand.Float64() * (price * 0.01)) + } + + times = append(times, cursor) + prices = append(prices, price) + } } return } @@ -220,7 +41,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) { priceSeries := chart.TimeSeries{ Name: "SPY", Style: chart.Style{ - Show: true, + Show: false, StrokeColor: chart.GetDefaultColor(0), }, XValues: xv, @@ -235,15 +56,9 @@ func drawChart(res http.ResponseWriter, req *http.Request) { graph := chart.Chart{ XAxis: chart.XAxis{ - Style: chart.Style{Show: true}, - TickPosition: chart.TickPositionBetweenTicks, - Range: &chart.MarketHoursRange{ - Min: seq.Times(xv...).Min().In(util.Date.Eastern()), - Max: seq.Times(xv...).Max().In(util.Date.Eastern()), - MarketOpen: util.NYSEOpen(), - MarketClose: util.NYSEClose(), - HolidayProvider: util.Date.IsNYSEHoliday, - }, + Style: chart.Style{Show: true, FontSize: 8, TextRotationDegrees: 45}, + TickPosition: chart.TickPositionUnderTick, + Range: &chart.MarketHoursRange{}, }, YAxis: chart.YAxis{ Style: chart.Style{Show: true}, diff --git a/candlestick_series.go b/candlestick_series.go index 3ce29df..cdd8112 100644 --- a/candlestick_series.go +++ b/candlestick_series.go @@ -60,7 +60,12 @@ func (cs CandlestickSeries) Len() int { } // GetValues returns the values at a given index. -func (cs CandlestickSeries) GetValues(index int) (time.Time, float64) { +func (cs CandlestickSeries) GetValues(index int) (float64, float64) { + return util.Time.ToFloat64(cs.XValues[index]), cs.YValues[index] +} + +// GetRawValues returns the values at a given index. +func (cs CandlestickSeries) GetRawValues(index int) (time.Time, float64) { return cs.XValues[index], cs.YValues[index] } @@ -78,7 +83,7 @@ func (cs CandlestickSeries) CandleValues() []CandleValue { var t time.Time var lv, v float64 - t, v = cs.GetValues(0) + t, v = cs.GetRawValues(0) year, month, day = t.Year(), int(t.Month()), t.Day() lastYear, lastMonth, lastDay = year, month, day @@ -92,7 +97,7 @@ func (cs CandlestickSeries) CandleValues() []CandleValue { lv = v for i := 1; i < totalValues; i++ { - t, v = cs.GetValues(i) + t, v = cs.GetRawValues(i) 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 diff --git a/draw.go b/draw.go index 0550a3e..ed17404 100644 --- a/draw.go +++ b/draw.go @@ -175,37 +175,34 @@ func (d draw) CandlestickSeries(r Renderer, canvasBox Box, xrange, yrange Range, candleValues := cs.CandleValues() - //calculate bar width? - seriesLength := len(candleValues) - barWidth := int(math.Floor(float64(xrange.GetDomain()) / float64(seriesLength))) - - bw2 := barWidth >> 1 - cb := canvasBox.Bottom cl := canvasBox.Left var cv CandleValue - for index := 0; index < seriesLength; index++ { + for index := 0; index < len(candleValues); index++ { cv = candleValues[index] y0 := yrange.Translate(cv.Open) y1 := yrange.Translate(cv.Close) - x := cl + xrange.Translate(util.Time.ToFloat64(cv.Timestamp)) + x0 := cl + xrange.Translate(util.Time.ToFloat64(util.Date.On(util.NYSEOpen(), cv.Timestamp))) + x1 := cl + xrange.Translate(util.Time.ToFloat64(util.Date.On(util.NYSEClose(), cv.Timestamp))) + + x := x0 + ((x1 - x0) >> 1) // draw open / close box. if cv.Open < cv.Close { d.Box(r, Box{ Top: cb - y0, - Left: x - bw2, - Right: x + bw2, + Left: x0, + Right: x1, Bottom: cb - y1, }, style.InheritFrom(Style{FillColor: ColorAlternateGreen})) } else { d.Box(r, Box{ Top: cb - y1, - Left: x - bw2, - Right: x + bw2, + Left: x0, + Right: x1, Bottom: cb - y0, }, style.InheritFrom(Style{FillColor: ColorRed})) } @@ -216,9 +213,17 @@ func (d draw) CandlestickSeries(r Renderer, canvasBox Box, xrange, yrange Range, style.InheritFrom(Style{StrokeColor: DefaultStrokeColor}).WriteToRenderer(r) + r.MoveTo(x0, cb-y0) + r.LineTo(x1, cb-y0) + r.Stroke() + r.MoveTo(x, cb-y0) r.LineTo(x, cb-y1) r.Stroke() + + r.MoveTo(x0, cb-y1) + r.LineTo(x1, cb-y1) + r.Stroke() } } diff --git a/market_hours_range.go b/market_hours_range.go index 6bbf200..2f12825 100644 --- a/market_hours_range.go +++ b/market_hours_range.go @@ -91,7 +91,7 @@ func (mhr *MarketHoursRange) SetDomain(domain int) { // GetHolidayProvider coalesces a userprovided holiday provider and the date.DefaultHolidayProvider. func (mhr MarketHoursRange) GetHolidayProvider() util.HolidayProvider { if mhr.HolidayProvider == nil { - return func(_ time.Time) bool { return false } + return util.Date.IsNYSEHoliday } return mhr.HolidayProvider } @@ -146,7 +146,6 @@ func (mhr *MarketHoursRange) GetTicks(r Renderer, defaults Style, vf ValueFormat } return GenerateContinuousTicks(r, mhr, false, defaults, vf) - } func (mhr *MarketHoursRange) measureTimes(r Renderer, defaults Style, vf ValueFormatter, times []time.Time) int { @@ -183,8 +182,8 @@ func (mhr MarketHoursRange) String() string { func (mhr MarketHoursRange) Translate(value float64) int { valueTime := util.Time.FromFloat64(value) valueTimeEastern := valueTime.In(util.Date.Eastern()) - totalSeconds := util.Date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider) - valueDelta := util.Date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider) + totalSeconds := util.Date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + valueDelta := util.Date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) translated := int((float64(valueDelta) / float64(totalSeconds)) * float64(mhr.Domain)) if mhr.IsDescending() { diff --git a/seq/time.go b/seq/time.go index fecab06..65c9bac 100644 --- a/seq/time.go +++ b/seq/time.go @@ -60,8 +60,8 @@ func (tu timeUtil) MarketDayCloses(from, to time.Time, marketOpen, marketClose t for cursor.Before(toClose) || cursor.Equal(toClose) { isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday()) if isValidTradingDay { - todayClose := util.Date.On(marketClose, cursor) - times = append(times, todayClose) + newValue := util.Date.NoonOn(cursor) + times = append(times, newValue) } cursor = util.Date.NextDay(cursor) diff --git a/xaxis.go b/xaxis.go index d97616c..d956f7e 100644 --- a/xaxis.go +++ b/xaxis.go @@ -156,7 +156,7 @@ func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick tx = tx - tb.Width()>>1 ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height() } else { - ty = canvasBox.Bottom + (2 * DefaultXAxisMargin) + ty = canvasBox.Bottom + (1.5 * DefaultXAxisMargin) } Draw.Text(r, t.Label, tx, ty, tickWithAxisStyle) maxTextHeight = util.Math.MaxInt(maxTextHeight, tb.Height())