package chart 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 ) // NYSEOpen is when the NYSE opens. func NYSEOpen() time.Time { return Date.Time(9, 30, 0, 0, Date.Eastern()) } // NYSEClose is when the NYSE closes. func NYSEClose() time.Time { return Date.Time(16, 0, 0, 0, Date.Eastern()) } // NASDAQOpen is when NASDAQ opens. func NASDAQOpen() time.Time { return Date.Time(9, 30, 0, 0, Date.Eastern()) } // NASDAQClose is when NASDAQ closes. func NASDAQClose() time.Time { return Date.Time(16, 0, 0, 0, Date.Eastern()) } // NYSEArcaOpen is when NYSEARCA opens. func NYSEArcaOpen() time.Time { return Date.Time(4, 0, 0, 0, Date.Eastern()) } // NYSEArcaClose is when NYSEARCA closes. func NYSEArcaClose() time.Time { return Date.Time(20, 0, 0, 0, Date.Eastern()) } // HolidayProvider is a function that returns if a given time falls on a holiday. type HolidayProvider func(time.Time) bool // defaultHolidayProvider implements `HolidayProvider` and just returns false. func defaultHolidayProvider(_ time.Time) bool { return false } var ( // Date contains utility functions that operate on dates. Date = &date{} ) type date struct{} // IsNYSEHoliday returns if a date was/is on a nyse holiday day. func (d date) IsNYSEHoliday(t time.Time) bool { te := t.In(d.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 } // IsNYSEArcaHoliday returns that returns if a given time falls on a holiday. func (d date) IsNYSEArcaHoliday(t time.Time) bool { return d.IsNYSEHoliday(t) } // IsNASDAQHoliday returns if a date was a NASDAQ holiday day. func (d date) IsNASDAQHoliday(t time.Time) bool { return d.IsNYSEHoliday(t) } // 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 { 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(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()) } // Optional returns a pointer reference to a given time. func (d date) Optional(t time.Time) *time.Time { return &t } // IsWeekDay returns if the day is a monday->friday. func (d date) IsWeekDay(day time.Weekday) bool { return !d.IsWeekendDay(day) } // IsWeekendDay returns if the day is a monday->friday. func (d date) IsWeekendDay(day time.Weekday) bool { return day == time.Saturday || day == time.Sunday } // Before returns if a timestamp is strictly before another date (ignoring hours, minutes etc.) func (d date) Before(before, reference time.Time) bool { tzAdjustedBefore := before.In(reference.Location()) if tzAdjustedBefore.Year() < reference.Year() { return true } if tzAdjustedBefore.Month() < reference.Month() { return true } 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 { afterLocalized := after.In(openTime.Location()) todaysOpen := d.On(openTime, afterLocalized) if isHoliday == nil { isHoliday = defaultHolidayProvider } todayIsValidTradingDay := d.IsWeekDay(todaysOpen.Weekday()) && !isHoliday(todaysOpen) if (afterLocalized.Equal(todaysOpen) || afterLocalized.Before(todaysOpen)) && todayIsValidTradingDay { return todaysOpen } for cursorDay := 1; cursorDay < 7; cursorDay++ { newDay := todaysOpen.AddDate(0, 0, cursorDay) isValidTradingDay := d.IsWeekDay(newDay.Weekday()) && !isHoliday(newDay) if isValidTradingDay { return d.On(openTime, newDay) } } panic("Have exhausted day window looking for next market open.") } // NextMarketClose returns the next market close after a given time. func (d date) NextMarketClose(after, closeTime time.Time, isHoliday HolidayProvider) time.Time { afterLocalized := after.In(closeTime.Location()) if isHoliday == nil { isHoliday = defaultHolidayProvider } todaysClose := d.On(closeTime, afterLocalized) if afterLocalized.Before(todaysClose) && d.IsWeekDay(todaysClose.Weekday()) && !isHoliday(todaysClose) { return todaysClose } if afterLocalized.Equal(todaysClose) { //rare but it might happen. return todaysClose } for cursorDay := 1; cursorDay < 6; cursorDay++ { newDay := todaysClose.AddDate(0, 0, cursorDay) if d.IsWeekDay(newDay.Weekday()) && !isHoliday(newDay) { return d.On(closeTime, newDay) } } panic("Have exhausted day window looking for next market close.") } // CalculateMarketSecondsBetween calculates the number of seconds the market was open between two dates. func (d date) CalculateMarketSecondsBetween(start, end, marketOpen, marketClose time.Time, isHoliday HolidayProvider) (seconds int64) { startEastern := start.In(d.Eastern()) endEastern := end.In(d.Eastern()) startMarketOpen := d.On(marketOpen, startEastern) startMarketClose := d.On(marketClose, startEastern) if !d.IsWeekendDay(startMarketOpen.Weekday()) && !isHoliday(startMarketOpen) { if (startEastern.Equal(startMarketOpen) || startEastern.After(startMarketOpen)) && startEastern.Before(startMarketClose) { if endEastern.Before(startMarketClose) { seconds += int64(endEastern.Sub(startEastern) / time.Second) } else { seconds += int64(startMarketClose.Sub(startEastern) / time.Second) } } } cursor := d.NextMarketOpen(startMarketClose, marketOpen, isHoliday) for d.Before(cursor, endEastern) { if d.IsWeekDay(cursor.Weekday()) && !isHoliday(cursor) { close := d.NextMarketClose(cursor, marketClose, isHoliday) seconds += int64(close.Sub(cursor) / time.Second) } cursor = cursor.AddDate(0, 0, 1) } finalMarketOpen := d.NextMarketOpen(cursor, marketOpen, isHoliday) finalMarketClose := d.NextMarketClose(cursor, marketClose, isHoliday) if endEastern.After(finalMarketOpen) { if endEastern.Before(finalMarketClose) { seconds += int64(endEastern.Sub(finalMarketOpen) / time.Second) } else { seconds += int64(finalMarketClose.Sub(finalMarketOpen) / time.Second) } } return } const ( _secondsPerHour = 60 * 60 _secondsPerDay = 60 * 60 * 24 ) func (d date) DiffDays(t1, t2 time.Time) (days int) { t1n := t1.Unix() t2n := t2.Unix() diff := t2n - t1n //yields seconds return int(diff / (_secondsPerDay)) } func (d date) DiffHours(t1, t2 time.Time) (hours int) { t1n := t1.Unix() t2n := t2.Unix() diff := t2n - t1n //yields seconds return int(diff / (_secondsPerHour)) } // NextDay returns the timestamp advanced a day. func (d date) NextDay(ts time.Time) time.Time { return ts.AddDate(0, 0, 1) } // NextHour returns the next timestamp on the hour. func (d date) NextHour(ts time.Time) time.Time { //advance a full hour ... advanced := ts.Add(time.Hour) minutes := time.Duration(advanced.Minute()) * time.Minute 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) } // Start returns the earliest (min) time in a list of times. func (d date) Start(times []time.Time) time.Time { if len(times) == 0 { return time.Time{} } start := times[0] for _, t := range times[1:] { if t.Before(start) { start = t } } return start } // Start returns the earliest (min) time in a list of times. func (d date) End(times []time.Time) time.Time { if len(times) == 0 { return time.Time{} } end := times[0] for _, t := range times[1:] { if t.After(end) { end = t } } return end }