date, nyse market hours range.

This commit is contained in:
Will Charczuk 2016-07-21 22:09:09 -07:00
parent c2f7c99c3f
commit c4066176cf
4 changed files with 462 additions and 0 deletions

305
date/date.go Normal file
View File

@ -0,0 +1,305 @@
package date
import (
"sync"
"time"
)
const (
// AllDaysMask is a bitmask of all the days of the week.
AllDaysMask = 1<<uint(time.Sunday) | 1<<uint(time.Monday) | 1<<uint(time.Tuesday) | 1<<uint(time.Wednesday) | 1<<uint(time.Thursday) | 1<<uint(time.Friday) | 1<<uint(time.Saturday)
// WeekDaysMask is a bitmask of all the weekdays of the week.
WeekDaysMask = 1<<uint(time.Monday) | 1<<uint(time.Tuesday) | 1<<uint(time.Wednesday) | 1<<uint(time.Thursday) | 1<<uint(time.Friday)
//WeekendDaysMask is a bitmask of the weekend days of the week.
WeekendDaysMask = 1<<uint(time.Sunday) | 1<<uint(time.Saturday)
)
var (
// DaysOfWeek are all the time.Weekday in an array for utility purposes.
DaysOfWeek = []time.Weekday{
time.Sunday,
time.Monday,
time.Tuesday,
time.Wednesday,
time.Thursday,
time.Friday,
time.Saturday,
}
// WeekDays are the business time.Weekday in an array.
WeekDays = []time.Weekday{
time.Monday,
time.Tuesday,
time.Wednesday,
time.Thursday,
time.Friday,
}
// WeekendDays are the weekend time.Weekday in an array.
WeekendDays = []time.Weekday{
time.Sunday,
time.Saturday,
}
//Epoch is unix epoc saved for utility purposes.
Epoch = time.Unix(0, 0)
)
var (
_easternLock sync.Mutex
_eastern *time.Location
)
// Eastern returns the eastern timezone.
func Eastern() *time.Location {
if _eastern == nil {
_easternLock.Lock()
defer _easternLock.Unlock()
if _eastern == nil {
_eastern, _ = time.LoadLocation("America/New_York")
}
}
return _eastern
}
// Optional returns a pointer reference to a given time.
func Optional(t time.Time) *time.Time {
return &t
}
// IsWeekDay returns if the day is a monday->friday.
func IsWeekDay(day time.Weekday) bool {
return !IsWeekendDay(day)
}
// IsWeekendDay returns if the day is a monday->friday.
func IsWeekendDay(day time.Weekday) bool {
return day == time.Saturday || day == time.Sunday
}
// BeforeDate returns if a timestamp is strictly before another date (ignoring hours, minutes etc.)
func BeforeDate(before, reference time.Time) bool {
if before.Year() < reference.Year() {
return true
}
if before.Month() < reference.Month() {
return true
}
return before.Year() == reference.Year() && before.Month() == reference.Month() && before.Day() < reference.Day()
}
// IsNYSEHoliday returns if a date was/is on a nyse holiday day.
func IsNYSEHoliday(t time.Time) bool {
te := t.In(Eastern())
if te.Year() == 2013 {
if te.Month() == 1 {
return te.Day() == 1 || te.Day() == 21
} else if te.Month() == 2 {
return te.Day() == 18
} else if te.Month() == 3 {
return te.Day() == 29
} else if te.Month() == 5 {
return te.Day() == 27
} else if te.Month() == 7 {
return te.Day() == 4
} else if te.Month() == 9 {
return te.Day() == 2
} else if te.Month() == 11 {
return te.Day() == 28
} else if te.Month() == 12 {
return te.Day() == 25
}
} else if te.Year() == 2014 {
if te.Month() == 1 {
return te.Day() == 1 || te.Day() == 20
} else if te.Month() == 2 {
return te.Day() == 17
} else if te.Month() == 4 {
return te.Day() == 18
} else if te.Month() == 5 {
return te.Day() == 26
} else if te.Month() == 7 {
return te.Day() == 4
} else if te.Month() == 9 {
return te.Day() == 1
} else if te.Month() == 11 {
return te.Day() == 27
} else if te.Month() == 12 {
return te.Day() == 25
}
} else if te.Year() == 2015 {
if te.Month() == 1 {
return te.Day() == 1 || te.Day() == 19
} else if te.Month() == 2 {
return te.Day() == 16
} else if te.Month() == 4 {
return te.Day() == 3
} else if te.Month() == 5 {
return te.Day() == 25
} else if te.Month() == 7 {
return te.Day() == 3
} else if te.Month() == 9 {
return te.Day() == 7
} else if te.Month() == 11 {
return te.Day() == 26
} else if te.Month() == 12 {
return te.Day() == 25
}
} else if te.Year() == 2016 {
if te.Month() == 1 {
return te.Day() == 1 || te.Day() == 18
} else if te.Month() == 2 {
return te.Day() == 15
} else if te.Month() == 3 {
return te.Day() == 25
} else if te.Month() == 5 {
return te.Day() == 30
} else if te.Month() == 7 {
return te.Day() == 4
} else if te.Month() == 9 {
return te.Day() == 5
} else if te.Month() == 11 {
return te.Day() == 24 || te.Day() == 25
} else if te.Month() == 12 {
return te.Day() == 26
}
} else if te.Year() == 2017 {
if te.Month() == 1 {
return te.Day() == 1 || te.Day() == 16
} else if te.Month() == 2 {
return te.Day() == 20
} else if te.Month() == 4 {
return te.Day() == 15
} else if te.Month() == 5 {
return te.Day() == 29
} else if te.Month() == 7 {
return te.Day() == 4
} else if te.Month() == 9 {
return te.Day() == 4
} else if te.Month() == 11 {
return te.Day() == 23
} else if te.Month() == 12 {
return te.Day() == 25
}
} else if te.Year() == 2018 {
if te.Month() == 1 {
return te.Day() == 1 || te.Day() == 15
} else if te.Month() == 2 {
return te.Day() == 19
} else if te.Month() == 3 {
return te.Day() == 30
} else if te.Month() == 5 {
return te.Day() == 28
} else if te.Month() == 7 {
return te.Day() == 4
} else if te.Month() == 9 {
return te.Day() == 3
} else if te.Month() == 11 {
return te.Day() == 22
} else if te.Month() == 12 {
return te.Day() == 25
}
}
return false
}
// MarketOpen returns 0930 on a given day.
func MarketOpen(on time.Time) time.Time {
onEastern := on.In(Eastern())
return time.Date(onEastern.Year(), onEastern.Month(), onEastern.Day(), 9, 30, 0, 0, Eastern())
}
// MarketClose returns 1600 on a given day.
func MarketClose(on time.Time) time.Time {
onEastern := on.In(Eastern())
return time.Date(onEastern.Year(), onEastern.Month(), onEastern.Day(), 16, 0, 0, 0, Eastern())
}
// NextMarketOpen returns the next market open after a given time.
func NextMarketOpen(after time.Time) time.Time {
afterEastern := after.In(Eastern())
todaysOpen := MarketOpen(afterEastern)
if afterEastern.Before(todaysOpen) && IsWeekDay(todaysOpen.Weekday()) && !IsNYSEHoliday(todaysOpen) {
return todaysOpen
}
if afterEastern.Equal(todaysOpen) { //rare but it might happen.
return todaysOpen
}
for cursorDay := 1; cursorDay < 6; cursorDay++ {
newDay := todaysOpen.AddDate(0, 0, cursorDay)
if IsWeekDay(newDay.Weekday()) && !IsNYSEHoliday(afterEastern) {
return time.Date(newDay.Year(), newDay.Month(), newDay.Day(), 9, 30, 0, 0, Eastern())
}
}
return Epoch //we should never reach this.
}
// NextMarketClose returns the next market close after a given time.
func NextMarketClose(after time.Time) time.Time {
afterEastern := after.In(Eastern())
todaysClose := MarketClose(afterEastern)
if afterEastern.Before(todaysClose) && IsWeekDay(todaysClose.Weekday()) && !IsNYSEHoliday(todaysClose) {
return todaysClose
}
if afterEastern.Equal(todaysClose) { //rare but it might happen.
return todaysClose
}
for cursorDay := 1; cursorDay < 6; cursorDay++ {
newDay := todaysClose.AddDate(0, 0, cursorDay)
if IsWeekDay(newDay.Weekday()) && !IsNYSEHoliday(newDay) {
return time.Date(newDay.Year(), newDay.Month(), newDay.Day(), 16, 0, 0, 0, Eastern())
}
}
return Epoch //we should never reach this.
}
// CalculateMarketSecondsBetween calculates the number of seconds the market was open between two dates.
func CalculateMarketSecondsBetween(start, end time.Time) (seconds int64) {
se := start.In(Eastern())
ee := end.In(Eastern())
startMarketOpen := NextMarketOpen(se)
startMarketClose := NextMarketClose(se)
if (se.Equal(startMarketOpen) || se.After(startMarketOpen)) && se.Before(startMarketClose) {
seconds += int64(startMarketClose.Sub(se) / time.Second)
}
cursor := NextMarketOpen(startMarketClose)
for BeforeDate(cursor, ee) {
if IsWeekDay(cursor.Weekday()) && !IsNYSEHoliday(cursor) {
close := NextMarketClose(cursor)
seconds += int64(close.Sub(cursor) / time.Second)
}
cursor = cursor.AddDate(0, 0, 1)
}
finalMarketOpen := NextMarketOpen(cursor)
finalMarketClose := NextMarketClose(cursor)
if end.After(finalMarketOpen) {
if end.Before(finalMarketClose) {
seconds += int64(end.Sub(finalMarketOpen) / time.Second)
} else {
seconds += int64(finalMarketClose.Sub(finalMarketOpen) / time.Second)
}
}
return
}
// Format returns a string representation of a date.
func format(t time.Time) string {
return t.Format("2006-01-02")
}
// Parse parses a date from a string.
func parse(str string) time.Time {
res, _ := time.Parse("2006-01-02", str)
return res
}

87
date/date_test.go Normal file
View File

@ -0,0 +1,87 @@
package date
import (
"testing"
"time"
assert "github.com/blendlabs/go-assert"
)
func TestBeforeDate(t *testing.T) {
assert := assert.New(t)
assert.True(BeforeDate(parse("2015-07-02"), parse("2016-07-01")))
assert.True(BeforeDate(parse("2016-06-01"), parse("2016-07-01")))
assert.True(BeforeDate(parse("2016-07-01"), parse("2016-07-02")))
assert.False(BeforeDate(parse("2016-07-01"), parse("2016-07-01")))
assert.False(BeforeDate(parse("2016-07-03"), parse("2016-07-01")))
assert.False(BeforeDate(parse("2016-08-03"), parse("2016-07-01")))
assert.False(BeforeDate(parse("2017-08-03"), parse("2016-07-01")))
}
func TestNextMarketOpen(t *testing.T) {
assert := assert.New(t)
beforeOpen := time.Date(2016, 07, 18, 9, 0, 0, 0, Eastern())
todayOpen := time.Date(2016, 07, 18, 9, 30, 0, 0, Eastern())
afterOpen := time.Date(2016, 07, 18, 9, 31, 0, 0, Eastern())
tomorrowOpen := time.Date(2016, 07, 19, 9, 30, 0, 0, Eastern())
afterFriday := time.Date(2016, 07, 22, 9, 31, 0, 0, Eastern())
mondayOpen := time.Date(2016, 07, 25, 9, 30, 0, 0, Eastern())
weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Eastern())
assert.True(todayOpen.Equal(NextMarketOpen(beforeOpen)))
assert.True(tomorrowOpen.Equal(NextMarketOpen(afterOpen)))
assert.True(mondayOpen.Equal(NextMarketOpen(afterFriday)))
assert.True(mondayOpen.Equal(NextMarketOpen(weekend)))
testRegression := time.Date(2016, 07, 18, 16, 0, 0, 0, Eastern())
shouldbe := time.Date(2016, 07, 19, 9, 30, 0, 0, Eastern())
assert.True(shouldbe.Equal(NextMarketOpen(testRegression)))
}
func TestNextMarketClose(t *testing.T) {
assert := assert.New(t)
beforeClose := time.Date(2016, 07, 18, 15, 0, 0, 0, Eastern())
todayClose := time.Date(2016, 07, 18, 16, 00, 0, 0, Eastern())
afterClose := time.Date(2016, 07, 18, 16, 1, 0, 0, Eastern())
tomorrowClose := time.Date(2016, 07, 19, 16, 00, 0, 0, Eastern())
afterFriday := time.Date(2016, 07, 22, 16, 1, 0, 0, Eastern())
mondayClose := time.Date(2016, 07, 25, 16, 0, 0, 0, Eastern())
weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Eastern())
assert.True(todayClose.Equal(NextMarketClose(beforeClose)))
assert.True(tomorrowClose.Equal(NextMarketClose(afterClose)))
assert.True(mondayClose.Equal(NextMarketClose(afterFriday)))
assert.True(mondayClose.Equal(NextMarketClose(weekend)))
}
func TestCalculateMarketSecondsBetween(t *testing.T) {
assert := assert.New(t)
start := time.Date(2016, 07, 18, 9, 30, 0, 0, Eastern())
end := time.Date(2016, 07, 22, 16, 00, 0, 0, Eastern())
shouldbe := 5 * 6.5 * 60 * 60
assert.Equal(shouldbe, CalculateMarketSecondsBetween(start, end))
}
func TestCalculateMarketSecondsBetweenLTM(t *testing.T) {
assert := assert.New(t)
start := time.Date(2015, 07, 01, 9, 30, 0, 0, Eastern())
end := time.Date(2016, 07, 01, 9, 30, 0, 0, Eastern())
shouldbe := 253 * 6.5 * 60 * 60 //253 full market days since this date last year.
assert.Equal(shouldbe, CalculateMarketSecondsBetween(start, end))
}

View File

@ -0,0 +1,65 @@
package chart
import (
"fmt"
"time"
"github.com/wcharczuk/go-chart/date"
)
// NYSEMarketHoursRange is a special type of range that compresses a time range into just the
// market (i.e. NYSE operating hours and days) range.
type NYSEMarketHoursRange struct {
Min time.Time
Max time.Time
Domain int
}
// GetMin returns the min value.
func (mhr NYSEMarketHoursRange) GetMin() float64 {
return TimeToFloat64(mhr.Min)
}
// GetMax returns the max value.
func (mhr NYSEMarketHoursRange) GetMax() float64 {
return TimeToFloat64(mhr.Max)
}
// SetMin sets the min value.
func (mhr *NYSEMarketHoursRange) SetMin(min float64) {
mhr.Min = Float64ToTime(min)
}
// SetMax sets the max value.
func (mhr *NYSEMarketHoursRange) SetMax(max float64) {
mhr.Max = Float64ToTime(max)
}
// GetDelta gets the delta.
func (mhr NYSEMarketHoursRange) GetDelta() float64 {
min := TimeToFloat64(mhr.Min)
max := TimeToFloat64(mhr.Min)
return max - min
}
// GetDomain gets the domain.
func (mhr NYSEMarketHoursRange) GetDomain() int {
return mhr.Domain
}
// SetDomain sets the domain.
func (mhr *NYSEMarketHoursRange) SetDomain(domain int) {
mhr.Domain = domain
}
func (mhr NYSEMarketHoursRange) String() string {
return fmt.Sprintf("MarketHoursRange [%s, %s] => %d", mhr.Min.Format(DefaultDateFormat), mhr.Max.Format(DefaultDateFormat), mhr.Domain)
}
// Translate maps a given value into the ContinuousRange space.
func (mhr NYSEMarketHoursRange) Translate(value float64) int {
valueTime := Float64ToTime(value)
deltaSeconds := date.CalculateMarketSecondsBetween(mhr.Min, mhr.Max)
valueDelta := date.CalculateMarketSecondsBetween(mhr.Min, valueTime)
return int(float64(valueDelta) / float64(deltaSeconds))
}

View File

@ -20,6 +20,11 @@ func TimeToFloat64(t time.Time) float64 {
return float64(t.UnixNano()) return float64(t.UnixNano())
} }
// Float64ToTime returns a time from a float64.
func Float64ToTime(tf float64) time.Time {
return time.Unix(0, int64(tf))
}
// MinAndMax returns both the min and max in one pass. // MinAndMax returns both the min and max in one pass.
func MinAndMax(values ...float64) (min float64, max float64) { func MinAndMax(values ...float64) (min float64, max float64) {
if len(values) == 0 { if len(values) == 0 {