This commit is contained in:
Will Charczuk 2017-05-14 16:33:48 -07:00
parent 7d1401898a
commit 7ba2992824
6 changed files with 403 additions and 36 deletions

View file

@ -0,0 +1,267 @@
package main
import (
"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},
}
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)
}
return
}
func drawChart(res http.ResponseWriter, req *http.Request) {
xv, yv := stockData()
priceSeries := chart.TimeSeries{
Name: "SPY",
Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultColor(0),
},
XValues: xv,
YValues: yv,
}
candleSeries := chart.CandlestickSeries{
Name: "SPY",
XValues: xv,
YValues: yv,
}
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,
},
},
YAxis: chart.YAxis{
Style: chart.Style{Show: true},
},
Series: []chart.Series{
candleSeries,
priceSeries,
},
}
res.Header().Set("Content-Type", "image/png")
err := graph.Render(chart.PNG, res)
if err != nil {
panic(err)
}
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

View file

@ -18,6 +18,11 @@ type CandleValue struct {
Close float64
}
// String returns a string value for the candle value.
func (cv CandleValue) String() string {
return fmt.Sprintf("candle %s high: %.2f low: %.2f open: %.2f close: %.2f", cv.Timestamp.Format("2006-01-02"), cv.High, cv.Low, cv.Open, cv.Close)
}
// IsZero returns if the value is zero or not.
func (cv CandleValue) IsZero() bool {
return cv.Timestamp.IsZero()
@ -29,7 +34,9 @@ type CandlestickSeries struct {
Name string
Style Style
YAxis YAxisType
InnerSeries ValuesProvider
XValues []time.Time
YValues []float64
}
// GetName implements Series.GetName.
@ -47,37 +54,45 @@ func (cs CandlestickSeries) GetYAxis() YAxisType {
return cs.YAxis
}
// Len returns the length of the series.
func (cs CandlestickSeries) Len() int {
return util.Math.MinInt(len(cs.XValues), len(cs.YValues))
}
// GetValues returns the values at a given index.
func (cs CandlestickSeries) GetValues(index int) (time.Time, float64) {
return cs.XValues[index], cs.YValues[index]
}
// CandleValues returns the candlestick values for each day represented by the inner series.
func (cs CandlestickSeries) CandleValues() []CandleValue {
// for each "day" represented by the inner series
// compute the open (i.e. the first value at or near market open)
// compute the close (i.e. the last value at or near market close)
// compute the high, or the max
// compute the low, or the min
totalValues := cs.InnerSeries.Len()
totalValues := cs.Len()
if totalValues == 0 {
return nil
}
var value CandleValue
var values []CandleValue
var lastYear, lastMonth, lastDay int
var year, month, day int
var tv float64
var t time.Time
var lv, v float64
tv, v = cs.InnerSeries.GetValues(0)
t = util.Time.FromFloat64(tv)
t, v = cs.GetValues(0)
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
lastYear, lastMonth, lastDay = year, month, day
value := CandleValue{
Timestamp: cs.newTimestamp(year, month, day),
Open: v,
Low: v,
High: v,
}
lv = v
for i := 1; i < totalValues; i++ {
tv, v = cs.InnerSeries.GetValues(i)
t = util.Time.FromFloat64(tv)
t, v = cs.GetValues(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
@ -87,11 +102,18 @@ func (cs CandlestickSeries) CandleValues() []CandleValue {
value = CandleValue{
Timestamp: cs.newTimestamp(year, month, day),
}
Open: v,
High: v,
Low: v,
}
lastYear = year
lastMonth = month
lastDay = day
} else {
value.Low = math.Min(value.Low, v)
value.High = math.Max(value.Low, v)
value.High = math.Max(value.High, v)
}
lv = v
}
@ -104,14 +126,17 @@ func (cs CandlestickSeries) newTimestamp(year, month, day int) time.Time {
// Render implements Series.Render.
func (cs CandlestickSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
//style := cs.Style.InheritFrom(defaults)
//Draw.CandlestickSeries(r, canvasBox, xrange, yrange, style, cs)
style := cs.Style.InheritFrom(defaults)
Draw.CandlestickSeries(r, canvasBox, xrange, yrange, style, cs)
}
// Validate validates the series.
func (cs CandlestickSeries) Validate() error {
if cs.InnerSeries == nil {
return fmt.Errorf("histogram series requires InnerSeries to be set")
if cs.XValues == nil {
return fmt.Errorf("candlestick series requires `XValues` to be set")
}
if cs.YValues == nil {
return fmt.Errorf("candlestick series requires `YValues` to be set")
}
return nil
}

View file

@ -10,21 +10,28 @@ import (
)
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
start := util.Date.On(util.NYSEOpen(), time.Date(2017, 05, 15, 0, 0, 0, 0, util.Date.Eastern()))
cursor := start
for day := 0; day < 60; day++ {
if util.Date.IsWeekendDay(cursor.Weekday()) {
cursor = start.AddDate(0, 0, day)
continue
}
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)
}
cursor = start.AddDate(0, 0, day)
}
return
}
@ -34,12 +41,10 @@ func TestCandlestickSeriesCandleValues(t *testing.T) {
xdata, ydata := generateDummyStockData()
candleSeries := CandlestickSeries{
InnerSeries: TimeSeries{
XValues: xdata,
YValues: ydata,
},
}
values := candleSeries.CandleValues()
assert.NotEmpty(values)
assert.Len(values, 43) // should be 60 days per the generator.
}

54
draw.go
View file

@ -168,6 +168,60 @@ func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s
}
}
func (d draw) CandlestickSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, cs CandlestickSeries) {
if cs.Len() == 0 {
return
}
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++ {
cv = candleValues[index]
y0 := yrange.Translate(cv.Open)
y1 := yrange.Translate(cv.Close)
x := cl + xrange.Translate(util.Time.ToFloat64(cv.Timestamp))
// draw open / close box.
if cv.Open < cv.Close {
d.Box(r, Box{
Top: cb - y0,
Left: x - bw2,
Right: x + bw2,
Bottom: cb - y1,
}, style.InheritFrom(Style{FillColor: ColorAlternateGreen}))
} else {
d.Box(r, Box{
Top: cb - y1,
Left: x - bw2,
Right: x + bw2,
Bottom: cb - y0,
}, style.InheritFrom(Style{FillColor: ColorRed}))
}
// draw high / low t bars
y0 = yrange.Translate(cv.High)
y1 = yrange.Translate(cv.Low)
style.InheritFrom(Style{StrokeColor: DefaultStrokeColor}).WriteToRenderer(r)
r.MoveTo(x, cb-y0)
r.LineTo(x, cb-y1)
r.Stroke()
}
}
// MeasureAnnotation measures how big an annotation would be.
func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) Box {
style.WriteToRenderer(r)

16
util/time_test.go Normal file
View file

@ -0,0 +1,16 @@
package util
import (
"testing"
"time"
assert "github.com/blendlabs/go-assert"
)
func TestTimeFromFloat64(t *testing.T) {
assert := assert.New(t)
now := time.Now()
assert.InTimeDelta(now, Time.FromFloat64(Time.ToFloat64(now)), time.Microsecond)
}

View file

@ -5,7 +5,7 @@ import "github.com/wcharczuk/go-chart/drawing"
// ValuesProvider is a type that produces values.
type ValuesProvider interface {
Len() int
GetValues(index int) (float64, float64)
GetValues(index int) (x float64, y float64)
}
// BoundedValuesProvider allows series to return a range.