package util import ( "sync" "time" ) const ( // AllDaysMask is a bitmask of all the days of the week. AllDaysMask = 1<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) }