/
calendar.go
210 lines (181 loc) · 7.39 KB
/
calendar.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package utils
import (
"github.com/rickar/cal/v2"
"github.com/rickar/cal/v2/us"
"go.uber.org/zap"
"time"
)
type TimeWindow struct {
Start time.Time
End time.Time
Location *time.Location
}
func NewTimeWindow(startHour, startMinute, endHour, endMinute int, location *time.Location) TimeWindow {
return TimeWindow{
Start: time.Date(0, 0, 0, startHour, startMinute, 0, 0, location),
End: time.Date(0, 0, 0, endHour, endMinute, 0, 0, location),
Location: location,
}
}
type TradingCalendar struct {
Calendar *cal.BusinessCalendar
OnOpen TimeWindow
OnClose TimeWindow
TradingHours TimeWindow
}
var US = "America/New_York"
// TradingWindowUS creates a new TradingWindow struct representing the standard trading hours of
// AMEX, ARCA, BATS, NYSE, NASDAQ, NYSEARCA.
// Here are the standard trading hours for these exchanges (in Eastern Time):
// - Pre-Market Trading Hours: 4:00 a.m. to 9:30 a.m.
// - Regular Trading Hours: 9:30 a.m. to 4:00 p.m.
// - After-Market Hours: 4:00 p.m. to 8:00 p.m.
func TradingWindowUS() (TimeWindow, error) {
location, err := time.LoadLocation(US)
if err != nil {
return TimeWindow{}, err
}
tradingHours := NewTimeWindow(9, 30, 16, 0, location)
return tradingHours, nil
}
func TradingWindowUSOnOpen() (TimeWindow, error) {
location, err := time.LoadLocation(US)
if err != nil {
return TimeWindow{}, err
}
tradingHours := NewTimeWindow(9, 45, 10, 15, location)
return tradingHours, nil
}
func TradingWindowUSOnClose() (TimeWindow, error) {
location, err := time.LoadLocation(US)
if err != nil {
return TimeWindow{}, err
}
tradingHours := NewTimeWindow(15, 15, 15, 45, location)
return tradingHours, nil
}
// NewTradingCalendarUS creates a new TradingCalendar with specified opening and closing trading windows.
// It initializes a business calendar 'c' and adds US holidays to it.
// The function takes two arguments: 'OnOpen' and 'OnClose', which are TradingWindow structs representing
// the opening and closing hours of the trading day, respectively.
// It returns a TradingCalendar struct.
func NewTradingCalendarUS() (*TradingCalendar, error) {
log.Info("Creating new trading calendar")
c := cal.NewBusinessCalendar()
// Add US Holidays
log.Debug("Add US holidays to calendar",
zap.Strings("holidays", []string{"NewYear", "MlkDay", "PresidentsDay", "MemorialDay", "Juneteenth",
"IndependenceDay", "LaborDay", "ThanksgivingDay", "ChristmasDay", "VeteransDay", "ColumbusDay"},
),
)
c.AddHoliday(
us.NewYear,
us.MlkDay,
us.PresidentsDay,
us.MemorialDay,
us.Juneteenth,
us.IndependenceDay,
us.LaborDay,
us.ThanksgivingDay,
us.ChristmasDay,
us.VeteransDay,
us.ColumbusDay,
)
tradingWindow, err := TradingWindowUS()
if err != nil {
return nil, err
}
onOpen, err := TradingWindowUSOnOpen()
if err != nil {
return nil, err
}
onClose, err := TradingWindowUSOnClose()
if err != nil {
return nil, err
}
return &TradingCalendar{
Calendar: c,
OnOpen: onOpen,
OnClose: onClose,
TradingHours: tradingWindow,
}, nil
}
// IsTradingDay checks if the provided time 't' falls on a trading day according to the calendar 'c'.
// It returns true if 't' is a trading day, and false otherwise.
func (c *TradingCalendar) IsTradingDay(t time.Time) bool {
return c.Calendar.IsWorkday(t)
}
// NextDayOnOpen Returns the trading window for the opening hours of the next trading day after the provided time "t".
func (c *TradingCalendar) NextDayOnOpen(t time.Time) TimeWindow {
nextYear, nextMonth, nextDay := c.NextBusinessDay(t).Date()
startHour, startMinute, _ := c.OnOpen.Start.Clock()
endHour, endMinute, _ := c.OnOpen.End.Clock()
start := time.Date(nextYear, nextMonth, nextDay, startHour, startMinute, 0, 0, c.OnOpen.Location).In(t.Location())
end := time.Date(nextYear, nextMonth, nextDay, endHour, endMinute, 0, 0, c.OnOpen.Location).In(t.Location())
log.Debug("Next day on open", zap.Time("start", start), zap.Time("end", end))
return TimeWindow{Start: start, End: end, Location: t.Location()}
}
// NextDayOnClose Returns the trading window for the closing hours of the next trading day after the provided time "t".
func (c *TradingCalendar) NextDayOnClose(t time.Time) TimeWindow {
nextYear, nextMonth, nextDay := c.NextBusinessDay(t).Date()
startHour, startMinute, _ := c.OnClose.Start.Clock()
endHour, endMinute, _ := c.OnClose.End.Clock()
start := time.Date(nextYear, nextMonth, nextDay, startHour, startMinute, 0, 0, c.OnClose.Location).In(t.Location())
end := time.Date(nextYear, nextMonth, nextDay, endHour, endMinute, 0, 0, c.OnClose.Location).In(t.Location())
log.Debug("Next day on close", zap.Time("start", start), zap.Time("end", end))
return TimeWindow{Start: start, End: end, Location: t.Location()}
}
// IsOnOpen checks if the provided time 't' falls within the opening hours of the trading day.
// It does so by converting the given time to the same location as the calendar.
// It returns true if 't' is within the opening hours, and false otherwise.
func (c *TradingCalendar) IsOnOpen(t time.Time) bool {
// convert given time to the same location as the calendar
targetLocation := c.OnOpen.Location
givenTime := t.In(targetLocation)
// setup time objects for comparison
startHour, startMinute, _ := c.OnOpen.Start.Clock()
endHour, endMinute, _ := c.OnOpen.End.Clock()
start := time.Date(givenTime.Year(), givenTime.Month(), givenTime.Day(), startHour, startMinute, 0, 0, targetLocation)
end := time.Date(givenTime.Year(), givenTime.Month(), givenTime.Day(), endHour, endMinute, 0, 0, targetLocation)
return givenTime.After(start) && givenTime.Before(end)
}
// IsOnClose checks if the provided time 't' falls within the closing hours of the trading day.
// It does so by converting the given time to the same location as the calendar.
// It returns true if 't' is within the closing hours, and false otherwise.
func (c *TradingCalendar) IsOnClose(t time.Time) bool {
// convert given time to the same location as the calendar
targetLocation := c.OnClose.Location
givenTime := t.In(targetLocation)
// setup time objects for comparison
startHour, startMinute, _ := c.OnClose.Start.Clock()
endHour, endMinute, _ := c.OnClose.End.Clock()
start := time.Date(givenTime.Year(), givenTime.Month(), givenTime.Day(), startHour, startMinute, 0, 0, targetLocation)
end := time.Date(givenTime.Year(), givenTime.Month(), givenTime.Day(), endHour, endMinute, 0, 0, targetLocation)
return givenTime.After(start) && givenTime.Before(end)
}
// NextBusinessDay returns the next business day after the given time t.
// It checks if the given time t is a workday according to the TradingCalendar's calendar.
// If t is not a workday (it's a holiday or a weekend), it adds one day and checks again.
// It continues this process until it finds a workday, which it then returns.
// This function does not account for business hours; it only checks the date.
// If the given time t is already a workday, it will return the same day.
//
// Parameters:
// t : A time.Time value representing the date to start from.
//
// Returns:
// The next business day (as a time.Time value) after the given time t, or the same day if t
// is already a business day.
func (c *TradingCalendar) NextBusinessDay(t time.Time) time.Time {
t = t.AddDate(0, 0, 1)
for {
switch {
case !c.Calendar.IsWorkday(t):
// If it's a holiday or weekend, add 1 day
t = t.AddDate(0, 0, 1)
default:
log.Info("Next business day", zap.Time("t", t))
return t
}
}
}