forked from mementum/backtrader
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtradingcal.py
More file actions
280 lines (216 loc) · 9.66 KB
/
tradingcal.py
File metadata and controls
280 lines (216 loc) · 9.66 KB
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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
#
# Copyright (C) 2015-2020 Daniel Rodriguez
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################
from __future__ import (absolute_import, division, print_function,
unicode_literals)
from datetime import datetime, timedelta, time
from .metabase import MetaParams
from backtrader.utils.py3 import string_types, with_metaclass
from backtrader.utils import UTC
__all__ = ['TradingCalendarBase', 'TradingCalendar', 'PandasMarketCalendar']
# Imprecission in the full time conversion to float would wrap over to next day
# if microseconds is 999999 as defined in time.max
_time_max = time(hour=23, minute=59, second=59, microsecond=999990)
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(7)
(ISONODAY, ISOMONDAY, ISOTUESDAY, ISOWEDNESDAY, ISOTHURSDAY, ISOFRIDAY,
ISOSATURDAY, ISOSUNDAY) = range(8)
WEEKEND = [SATURDAY, SUNDAY]
ISOWEEKEND = [ISOSATURDAY, ISOSUNDAY]
ONEDAY = timedelta(days=1)
class TradingCalendarBase(with_metaclass(MetaParams, object)):
def _nextday(self, day):
'''
Returns the next trading day (datetime/date instance) after ``day``
(datetime/date instance) and the isocalendar components
The return value is a tuple with 2 components: (nextday, (y, w, d))
'''
raise NotImplementedError
def schedule(self, day):
'''
Returns a tuple with the opening and closing times (``datetime.time``)
for the given ``date`` (``datetime/date`` instance)
'''
raise NotImplementedError
def nextday(self, day):
'''
Returns the next trading day (datetime/date instance) after ``day``
(datetime/date instance)
'''
return self._nextday(day)[0] # 1st ret elem is next day
def nextday_week(self, day):
'''
Returns the iso week number of the next trading day, given a ``day``
(datetime/date) instance
'''
self._nextday(day)[1][1] # 2 elem is isocal / 0 - y, 1 - wk, 2 - day
def last_weekday(self, day):
'''
Returns ``True`` if the given ``day`` (datetime/date) instance is the
last trading day of this week
'''
# Next day must be greater than day. If the week changes is enough for
# a week change even if the number is smaller (year change)
return day.isocalendar()[1] != self._nextday(day)[1][1]
def last_monthday(self, day):
'''
Returns ``True`` if the given ``day`` (datetime/date) instance is the
last trading day of this month
'''
# Next day must be greater than day. If the week changes is enough for
# a week change even if the number is smaller (year change)
return day.month != self._nextday(day)[0].month
def last_yearday(self, day):
'''
Returns ``True`` if the given ``day`` (datetime/date) instance is the
last trading day of this month
'''
# Next day must be greater than day. If the week changes is enough for
# a week change even if the number is smaller (year change)
return day.year != self._nextday(day)[0].year
class TradingCalendar(TradingCalendarBase):
'''
Wrapper of ``pandas_market_calendars`` for a trading calendar. The package
``pandas_market_calendar`` must be installed
Params:
- ``open`` (default ``time.min``)
Regular start of the session
- ``close`` (default ``time.max``)
Regular end of the session
- ``holidays`` (default ``[]``)
List of non-trading days (``datetime.datetime`` instances)
- ``earlydays`` (default ``[]``)
List of tuples determining the date and opening/closing times of days
which do not conform to the regular trading hours where each tuple has
(``datetime.datetime``, ``datetime.time``, ``datetime.time`` )
- ``offdays`` (default ``ISOWEEKEND``)
A list of weekdays in ISO format (Monday: 1 -> Sunday: 7) in which the
market doesn't trade. This is usually Saturday and Sunday and hence the
default
'''
params = (
('open', time.min),
('close', _time_max),
('holidays', []), # list of non trading days (date)
('earlydays', []), # list of tuples (date, opentime, closetime)
('offdays', ISOWEEKEND), # list of non trading (isoweekdays)
)
def __init__(self):
self._earlydays = [x[0] for x in self.p.earlydays] # speed up searches
def _nextday(self, day):
'''
Returns the next trading day (datetime/date instance) after ``day``
(datetime/date instance) and the isocalendar components
The return value is a tuple with 2 components: (nextday, (y, w, d))
'''
while True:
day += ONEDAY
isocal = day.isocalendar()
if isocal[2] in self.p.offdays or day in self.p.holidays:
continue
return day, isocal
def schedule(self, day, tz=None):
'''
Returns the opening and closing times for the given ``day``. If the
method is called, the assumption is that ``day`` is an actual trading
day
The return value is a tuple with 2 components: opentime, closetime
'''
while True:
dt = day.date()
try:
i = self._earlydays.index(dt)
o, c = self.p.earlydays[i][1:]
except ValueError: # not found
o, c = self.p.open, self.p.close
closing = datetime.combine(dt, c)
if tz is not None:
closing = tz.localize(closing).astimezone(UTC)
closing = closing.replace(tzinfo=None)
if day > closing: # current time over eos
day += ONEDAY
continue
opening = datetime.combine(dt, o)
if tz is not None:
opening = tz.localize(opening).astimezone(UTC)
opening = opening.replace(tzinfo=None)
return opening, closing
class PandasMarketCalendar(TradingCalendarBase):
'''
Wrapper of ``pandas_market_calendars`` for a trading calendar. The package
``pandas_market_calendar`` must be installed
Params:
- ``calendar`` (default ``None``)
The param ``calendar`` accepts the following:
- string: the name of one of the calendars supported, for example
`NYSE`. The wrapper will attempt to get a calendar instance
- calendar instance: as returned by ``get_calendar('NYSE')``
- ``cachesize`` (default ``365``)
Number of days to cache in advance for lookup
See also:
- https://github.com/rsheftel/pandas_market_calendars
- http://pandas-market-calendars.readthedocs.io/
'''
params = (
('calendar', None), # A pandas_market_calendars instance or exch name
('cachesize', 365), # Number of days to cache in advance
)
def __init__(self):
self._calendar = self.p.calendar
if isinstance(self._calendar, string_types): # use passed mkt name
import pandas_market_calendars as mcal
self._calendar = mcal.get_calendar(self._calendar)
import pandas as pd # guaranteed because of pandas_market_calendars
self.dcache = pd.DatetimeIndex([0.0])
self.idcache = pd.DataFrame(index=pd.DatetimeIndex([0.0]))
self.csize = timedelta(days=self.p.cachesize)
def _nextday(self, day):
'''
Returns the next trading day (datetime/date instance) after ``day``
(datetime/date instance) and the isocalendar components
The return value is a tuple with 2 components: (nextday, (y, w, d))
'''
day += ONEDAY
while True:
i = self.dcache.searchsorted(day)
if i == len(self.dcache):
# keep a cache of 1 year to speed up searching
self.dcache = self._calendar.valid_days(day, day + self.csize)
continue
d = self.dcache[i].to_pydatetime()
return d, d.isocalendar()
def schedule(self, day, tz=None):
'''
Returns the opening and closing times for the given ``day``. If the
method is called, the assumption is that ``day`` is an actual trading
day
The return value is a tuple with 2 components: opentime, closetime
'''
while True:
i = self.idcache.index.searchsorted(day.date())
if i == len(self.idcache):
# keep a cache of 1 year to speed up searching
self.idcache = self._calendar.schedule(day, day + self.csize)
continue
st = (x.tz_localize(None) for x in self.idcache.iloc[i, 0:2])
opening, closing = st # Get utc naive times
if day > closing: # passed time is over the sessionend
day += ONEDAY # wrap over to next day
continue
return opening.to_pydatetime(), closing.to_pydatetime()