date, nyse market hours range.
This commit is contained in:
parent
c2f7c99c3f
commit
c4066176cf
4 changed files with 462 additions and 0 deletions
305
date/date.go
Normal file
305
date/date.go
Normal 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
87
date/date_test.go
Normal 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))
|
||||
}
|
65
nyse_market_hours_range.go
Normal file
65
nyse_market_hours_range.go
Normal 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))
|
||||
}
|
5
util.go
5
util.go
|
@ -20,6 +20,11 @@ func TimeToFloat64(t time.Time) float64 {
|
|||
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.
|
||||
func MinAndMax(values ...float64) (min float64, max float64) {
|
||||
if len(values) == 0 {
|
||||
|
|
Loading…
Reference in a new issue