market hours tweaks.

This commit is contained in:
Will Charczuk 2016-08-01 00:50:32 -07:00
parent c3a066aecd
commit b1cd8bd2e3
8 changed files with 253 additions and 39 deletions

64
date.go
View File

@ -52,22 +52,22 @@ var (
var (
// NYSEOpen is when the NYSE opens.
NYSEOpen = Date.ClockTime(9, 30, 0, 0, Date.Eastern())
NYSEOpen = Date.Time(9, 30, 0, 0, Date.Eastern())
// NYSEClose is when the NYSE closes.
NYSEClose = Date.ClockTime(16, 0, 0, 0, Date.Eastern())
NYSEClose = Date.Time(16, 0, 0, 0, Date.Eastern())
// NASDAQOpen is when NASDAQ opens.
NASDAQOpen = Date.ClockTime(9, 30, 0, 0, Date.Eastern())
NASDAQOpen = Date.Time(9, 30, 0, 0, Date.Eastern())
// NASDAQClose is when NASDAQ closes.
NASDAQClose = Date.ClockTime(16, 0, 0, 0, Date.Eastern())
NASDAQClose = Date.Time(16, 0, 0, 0, Date.Eastern())
// NYSEArcaOpen is when NYSEARCA opens.
NYSEArcaOpen = Date.ClockTime(4, 0, 0, 0, Date.Eastern())
NYSEArcaOpen = Date.Time(4, 0, 0, 0, Date.Eastern())
// NYSEArcaClose is when NYSEARCA closes.
NYSEArcaClose = Date.ClockTime(20, 0, 0, 0, Date.Eastern())
NYSEArcaClose = Date.Time(20, 0, 0, 0, Date.Eastern())
)
// HolidayProvider is a function that returns if a given time falls on a holiday.
@ -220,17 +220,22 @@ func (d date) Eastern() *time.Location {
return _eastern
}
// ClockTime returns a new time.Time for the given clock components.
func (d date) ClockTime(hour, min, sec, nsec int, loc *time.Location) time.Time {
// Time returns a new time.Time for the given clock components.
func (d date) Time(hour, min, sec, nsec int, loc *time.Location) time.Time {
return time.Date(0, 0, 0, hour, min, sec, nsec, loc)
}
func (d date) Date(year, month, day int, loc *time.Location) time.Time {
return time.Date(year, time.Month(month), day, 12, 0, 0, 0, loc)
}
// On returns the clock components of clock (hour,minute,second) on the date components of d.
func (d date) On(clock, cd time.Time) time.Time {
return time.Date(cd.Year(), cd.Month(), cd.Day(), clock.Hour(), clock.Minute(), clock.Second(), clock.Nanosecond(), clock.Location())
tzAdjusted := cd.In(clock.Location())
return time.Date(tzAdjusted.Year(), tzAdjusted.Month(), tzAdjusted.Day(), clock.Hour(), clock.Minute(), clock.Second(), clock.Nanosecond(), clock.Location())
}
// NoonOn is a shortcut for On(ClockTime(12,0,0), cd) a.k.a. noon on a given date.
// NoonOn is a shortcut for On(Time(12,0,0), cd) a.k.a. noon on a given date.
func (d date) NoonOn(cd time.Time) time.Time {
return time.Date(cd.Year(), cd.Month(), cd.Day(), 12, 0, 0, 0, cd.Location())
}
@ -252,19 +257,20 @@ func (d date) IsWeekendDay(day time.Weekday) bool {
// Before returns if a timestamp is strictly before another date (ignoring hours, minutes etc.)
func (d date) Before(before, reference time.Time) bool {
if before.Year() < reference.Year() {
tzAdjustedBefore := before.In(reference.Location())
if tzAdjustedBefore.Year() < reference.Year() {
return true
}
if before.Month() < reference.Month() {
if tzAdjustedBefore.Month() < reference.Month() {
return true
}
return before.Year() == reference.Year() && before.Month() == reference.Month() && before.Day() < reference.Day()
return tzAdjustedBefore.Year() == reference.Year() && tzAdjustedBefore.Month() == reference.Month() && tzAdjustedBefore.Day() < reference.Day()
}
// NextMarketOpen returns the next market open after a given time.
func (d date) NextMarketOpen(after, openTime time.Time, isHoliday HolidayProvider) time.Time {
afterEastern := after.In(d.Eastern())
todaysOpen := d.On(openTime, afterEastern)
afterLocalized := after.In(openTime.Location())
todaysOpen := d.On(openTime, afterLocalized)
if isHoliday == nil {
isHoliday = defaultHolidayProvider
@ -272,7 +278,7 @@ func (d date) NextMarketOpen(after, openTime time.Time, isHoliday HolidayProvide
todayIsValidTradingDay := d.IsWeekDay(todaysOpen.Weekday()) && !isHoliday(todaysOpen)
if (afterEastern.Equal(todaysOpen) || afterEastern.Before(todaysOpen)) && todayIsValidTradingDay {
if (afterLocalized.Equal(todaysOpen) || afterLocalized.Before(todaysOpen)) && todayIsValidTradingDay {
return todaysOpen
}
@ -288,18 +294,18 @@ func (d date) NextMarketOpen(after, openTime time.Time, isHoliday HolidayProvide
// NextMarketClose returns the next market close after a given time.
func (d date) NextMarketClose(after, closeTime time.Time, isHoliday HolidayProvider) time.Time {
afterEastern := after.In(d.Eastern())
afterLocalized := after.In(closeTime.Location())
if isHoliday == nil {
isHoliday = defaultHolidayProvider
}
todaysClose := d.On(closeTime, afterEastern)
if afterEastern.Before(todaysClose) && d.IsWeekDay(todaysClose.Weekday()) && !isHoliday(todaysClose) {
todaysClose := d.On(closeTime, afterLocalized)
if afterLocalized.Before(todaysClose) && d.IsWeekDay(todaysClose.Weekday()) && !isHoliday(todaysClose) {
return todaysClose
}
if afterEastern.Equal(todaysClose) { //rare but it might happen.
if afterLocalized.Equal(todaysClose) { //rare but it might happen.
return todaysClose
}
@ -376,3 +382,21 @@ func (d date) NextHour(ts time.Time) time.Time {
final := advanced.Add(-minutes)
return time.Date(final.Year(), final.Month(), final.Day(), final.Hour(), 0, 0, 0, final.Location())
}
// NextDayOfWeek returns the next instance of a given weekday after a given timestamp.
func (d date) NextDayOfWeek(after time.Time, dayOfWeek time.Weekday) time.Time {
afterWeekday := after.Weekday()
if afterWeekday == dayOfWeek {
return after.AddDate(0, 0, 7)
}
// 1 vs 5 ~ add 4 days
if afterWeekday < dayOfWeek {
dayDelta := int(dayOfWeek - afterWeekday)
return after.AddDate(0, 0, dayDelta)
}
// 5 vs 1, add 7-(5-1) ~ 3 days
dayDelta := 7 - int(afterWeekday-dayOfWeek)
return after.AddDate(0, 0, dayDelta)
}

View File

@ -12,6 +12,54 @@ func parse(v string) time.Time {
return ts
}
func TestDateTime(t *testing.T) {
assert := assert.New(t)
ts := Date.Time(5, 6, 7, 8, time.UTC)
assert.Equal(05, ts.Hour())
assert.Equal(06, ts.Minute())
assert.Equal(07, ts.Second())
assert.Equal(8, ts.Nanosecond())
assert.Equal(time.UTC, ts.Location())
}
func TestDateDate(t *testing.T) {
assert := assert.New(t)
ts := Date.Date(2015, 5, 6, time.UTC)
assert.Equal(2015, ts.Year())
assert.Equal(5, ts.Month())
assert.Equal(6, ts.Day())
assert.Equal(time.UTC, ts.Location())
}
func TestDateOn(t *testing.T) {
assert := assert.New(t)
ts := Date.On(Date.Time(5, 4, 3, 2, time.UTC), Date.Date(2016, 6, 7, Date.Eastern()))
assert.Equal(2016, ts.Year())
assert.Equal(6, ts.Month())
assert.Equal(7, ts.Day())
assert.Equal(5, ts.Hour())
assert.Equal(4, ts.Minute())
assert.Equal(3, ts.Second())
assert.Equal(2, ts.Nanosecond())
assert.Equal(time.UTC, ts.Location())
}
func TestDateNoonOn(t *testing.T) {
assert := assert.New(t)
noon := Date.NoonOn(time.Date(2016, 04, 03, 02, 01, 0, 0, time.UTC))
assert.Equal(2016, noon.Year())
assert.Equal(4, noon.Month())
assert.Equal(3, noon.Day())
assert.Equal(12, noon.Hour())
assert.Equal(0, noon.Minute())
assert.Equal(time.UTC, noon.Location())
}
func TestDateBefore(t *testing.T) {
assert := assert.New(t)
@ -25,6 +73,17 @@ func TestDateBefore(t *testing.T) {
assert.False(Date.Before(parse("2017-08-03"), parse("2016-07-01")))
}
func TestDateBeforeHandlesTimezones(t *testing.T) {
assert := assert.New(t)
tuesdayUTC := time.Date(2016, 8, 02, 22, 00, 0, 0, time.UTC)
mondayUTC := time.Date(2016, 8, 01, 1, 00, 0, 0, time.UTC)
sundayEST := time.Date(2016, 7, 31, 22, 00, 0, 0, Date.Eastern())
assert.True(Date.Before(sundayEST, tuesdayUTC))
assert.False(Date.Before(sundayEST, mondayUTC))
}
func TestNextMarketOpen(t *testing.T) {
assert := assert.New(t)
@ -44,6 +103,10 @@ func TestNextMarketOpen(t *testing.T) {
assert.True(mondayOpen.Equal(Date.NextMarketOpen(afterFriday, NYSEOpen, Date.IsNYSEHoliday)))
assert.True(mondayOpen.Equal(Date.NextMarketOpen(weekend, NYSEOpen, Date.IsNYSEHoliday)))
assert.Equal(Date.Eastern(), todayOpen.Location())
assert.Equal(Date.Eastern(), tomorrowOpen.Location())
assert.Equal(Date.Eastern(), mondayOpen.Location())
testRegression := time.Date(2016, 07, 18, 16, 0, 0, 0, Date.Eastern())
shouldbe := time.Date(2016, 07, 19, 9, 30, 0, 0, Date.Eastern())
@ -68,6 +131,10 @@ func TestNextMarketClose(t *testing.T) {
assert.True(tomorrowClose.Equal(Date.NextMarketClose(afterClose, NYSEClose, Date.IsNYSEHoliday)))
assert.True(mondayClose.Equal(Date.NextMarketClose(afterFriday, NYSEClose, Date.IsNYSEHoliday)))
assert.True(mondayClose.Equal(Date.NextMarketClose(weekend, NYSEClose, Date.IsNYSEHoliday)))
assert.Equal(Date.Eastern(), todayClose.Location())
assert.Equal(Date.Eastern(), tomorrowClose.Location())
assert.Equal(Date.Eastern(), mondayClose.Location())
}
func TestCalculateMarketSecondsBetween(t *testing.T) {
@ -120,3 +187,38 @@ func TestDateNextHour(t *testing.T) {
assert.Equal(12, next.Hour())
}
func TestDateNextDayOfWeek(t *testing.T) {
assert := assert.New(t)
weds := Date.Date(2016, 8, 10, time.UTC)
fri := Date.Date(2016, 8, 12, time.UTC)
sun := Date.Date(2016, 8, 14, time.UTC)
mon := Date.Date(2016, 8, 15, time.UTC)
weds2 := Date.Date(2016, 8, 17, time.UTC)
nextFri := Date.NextDayOfWeek(weds, time.Friday)
nextSunday := Date.NextDayOfWeek(weds, time.Sunday)
nextMonday := Date.NextDayOfWeek(weds, time.Monday)
nextWeds := Date.NextDayOfWeek(weds, time.Wednesday)
assert.Equal(fri.Year(), nextFri.Year())
assert.Equal(fri.Month(), nextFri.Month())
assert.Equal(fri.Day(), nextFri.Day())
assert.Equal(sun.Year(), nextSunday.Year())
assert.Equal(sun.Month(), nextSunday.Month())
assert.Equal(sun.Day(), nextSunday.Day())
assert.Equal(mon.Year(), nextMonday.Year())
assert.Equal(mon.Month(), nextMonday.Month())
assert.Equal(mon.Day(), nextMonday.Day())
assert.Equal(weds2.Year(), nextWeds.Year())
assert.Equal(weds2.Month(), nextWeds.Month())
assert.Equal(weds2.Day(), nextWeds.Day())
assert.Equal(time.UTC, nextFri.Location())
assert.Equal(time.UTC, nextSunday.Location())
assert.Equal(time.UTC, nextMonday.Location())
}

View File

@ -2,14 +2,13 @@ package main
import (
"net/http"
"time"
"github.com/wcharczuk/go-chart"
)
func drawChart(res http.ResponseWriter, req *http.Request) {
start := time.Date(2016, 07, 04, 12, 0, 0, 0, chart.Date.Eastern())
end := time.Date(2016, 07, 06, 12, 0, 0, 0, chart.Date.Eastern())
start := chart.Date.Date(2016, 6, 20, chart.Date.Eastern())
end := chart.Date.Date(2016, 07, 21, chart.Date.Eastern())
xv := chart.Sequence.MarketHours(start, end, chart.NYSEOpen, chart.NYSEClose, chart.Date.IsNYSEHoliday)
yv := chart.Sequence.RandomWithAverage(len(xv), 200, 10)

View File

@ -21,6 +21,11 @@ type MarketHoursRange struct {
Domain int
}
// GetTimezone returns the timezone for the market hours range.
func (mhr MarketHoursRange) GetTimezone() *time.Location {
return mhr.GetMarketOpen().Location()
}
// IsZero returns if the range is setup or not.
func (mhr MarketHoursRange) IsZero() bool {
return mhr.Min.IsZero() && mhr.Max.IsZero()
@ -48,11 +53,13 @@ func (mhr MarketHoursRange) GetEffectiveMax() time.Time {
// SetMin sets the min value.
func (mhr *MarketHoursRange) SetMin(min float64) {
mhr.Min = Float64ToTime(min)
mhr.Min = mhr.Min.In(mhr.GetTimezone())
}
// SetMax sets the max value.
func (mhr *MarketHoursRange) SetMax(max float64) {
mhr.Max = Float64ToTime(max)
mhr.Max = mhr.Max.In(mhr.GetTimezone())
}
// GetDelta gets the delta.
@ -99,18 +106,38 @@ 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 {
println("GetTicks() domain:", mhr.Domain)
times := Sequence.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 = Sequence.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)
}
return mhr.makeTicks(vf, Sequence.MarketDayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()))
times = Sequence.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 = Sequence.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 = Sequence.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)
}
return GenerateContinuousTicks(r, mhr, false, defaults, vf)
}
func (mhr *MarketHoursRange) measureTimes(r Renderer, defaults Style, vf ValueFormatter, times []time.Time) int {
@ -135,13 +162,12 @@ func (mhr *MarketHoursRange) makeTicks(vf ValueFormatter, times []time.Time) []T
Value: TimeToFloat64(t),
Label: vf(t),
}
println("make tick =>", vf(t))
}
return ticks
}
func (mhr MarketHoursRange) String() string {
return fmt.Sprintf("MarketHoursRange [%s, %s] => %d", mhr.Min.Format(DefaultDateMinuteFormat), mhr.Max.Format(DefaultDateMinuteFormat), mhr.Domain)
return fmt.Sprintf("MarketHoursRange [%s, %s] => %d", mhr.Min.Format(time.RFC3339), mhr.Max.Format(time.RFC3339), mhr.Domain)
}
// Translate maps a given value into the ContinuousRange space.

View File

@ -43,18 +43,30 @@ func TestMarketHoursRangeTranslate(t *testing.T) {
func TestMarketHoursRangeGetTicks(t *testing.T) {
assert := assert.New(t)
r := &MarketHoursRange{
Min: time.Date(2016, 07, 18, 9, 30, 0, 0, Date.Eastern()),
Max: time.Date(2016, 07, 22, 16, 00, 0, 0, Date.Eastern()),
r, err := PNG(1024, 1024)
assert.Nil(err)
f, err := GetDefaultFont()
assert.Nil(err)
defaults := Style{
Font: f,
FontSize: 10,
FontColor: ColorBlack,
}
ra := &MarketHoursRange{
Min: Date.On(NYSEOpen, Date.Date(2016, 07, 18, Date.Eastern())),
Max: Date.On(NYSEClose, Date.Date(2016, 07, 22, Date.Eastern())),
MarketOpen: NYSEOpen,
MarketClose: NYSEClose,
HolidayProvider: Date.IsNYSEHoliday,
Domain: 1000,
Domain: 1024,
}
ticks := r.GetTicks(TimeValueFormatter)
ticks := ra.GetTicks(r, defaults, TimeValueFormatter)
assert.NotEmpty(ticks)
assert.Len(ticks, 24)
assert.NotEqual(TimeToFloat64(r.Min), ticks[0].Value)
assert.Len(ticks, 5)
assert.NotEqual(TimeToFloat64(ra.Min), ticks[0].Value)
assert.NotEmpty(ticks[0].Label)
}

View File

@ -99,7 +99,7 @@ func (s sequence) MarketHourQuarters(from, to time.Time, marketOpen, marketClose
if isValidTradingDay {
todayOpen := Date.On(marketOpen, cursor)
todayNoon := Date.NoonOn(cursor)
today2pm := Date.On(Date.ClockTime(2, 0, 0, 0, cursor.Location()), cursor)
today2pm := Date.On(Date.Time(14, 0, 0, 0, cursor.Location()), cursor)
todayClose := Date.On(marketClose, cursor)
times = append(times, todayOpen, todayNoon, today2pm, todayClose)
}
@ -124,3 +124,36 @@ func (s sequence) MarketDayCloses(from, to time.Time, marketOpen, marketClose ti
}
return times
}
func (s sequence) MarketDayAlternateCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time {
var times []time.Time
cursor := Date.On(marketOpen, from)
toClose := Date.On(marketClose, to)
for cursor.Before(toClose) || cursor.Equal(toClose) {
isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday())
if isValidTradingDay {
todayClose := Date.On(marketClose, cursor)
times = append(times, todayClose)
}
cursor = cursor.AddDate(0, 0, 2)
}
return times
}
func (s sequence) MarketDayMondayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time {
var times []time.Time
cursor := Date.On(marketClose, from)
toClose := Date.On(marketClose, to)
for cursor.Equal(toClose) || cursor.Before(toClose) {
isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday())
if isValidTradingDay {
times = append(times, cursor)
}
println("advance to next monday", cursor.Format(DefaultDateFormat))
cursor = Date.NextDayOfWeek(cursor, time.Monday)
println(cursor.Format(DefaultDateFormat))
}
return times
}

View File

@ -22,5 +22,24 @@ func TestSequenceMarketHours(t *testing.T) {
today := time.Date(2016, 07, 01, 12, 0, 0, 0, Date.Eastern())
mh := Sequence.MarketHours(today, today, NYSEOpen, NYSEClose, Date.IsNYSEHoliday)
assert.Len(mh, 7)
assert.Len(mh, 8)
assert.Equal(Date.Eastern(), mh[0].Location())
}
func TestSequenceMarketQuarters(t *testing.T) {
assert := assert.New(t)
today := time.Date(2016, 07, 01, 12, 0, 0, 0, Date.Eastern())
mh := Sequence.MarketHourQuarters(today, today, NYSEOpen, NYSEClose, Date.IsNYSEHoliday)
assert.Len(mh, 4)
assert.Equal(9, mh[0].Hour())
assert.Equal(30, mh[0].Minute())
assert.Equal(Date.Eastern(), mh[0].Location())
assert.Equal(12, mh[1].Hour())
assert.Equal(00, mh[1].Minute())
assert.Equal(Date.Eastern(), mh[1].Location())
assert.Equal(14, mh[2].Hour())
assert.Equal(00, mh[2].Minute())
assert.Equal(Date.Eastern(), mh[2].Location())
}

View File

@ -72,7 +72,7 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
tp := xa.GetTickPosition()
var left, right, top, bottom = math.MaxInt32, 0, math.MaxInt32, 0
var left, right, bottom = math.MaxInt32, 0, 0
for index, t := range ticks {
v := t.Value
tickStyle.GetTextOptions().WriteToRenderer(r)
@ -94,14 +94,13 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
break
}
top = Math.MinInt(top, canvasBox.Bottom)
left = Math.MinInt(left, ltx)
right = Math.MaxInt(right, rtx)
bottom = Math.MaxInt(bottom, ty)
}
return Box{
Top: top,
Top: canvasBox.Bottom,
Left: left,
Right: right,
Bottom: bottom,