Skip to content

Commit

Permalink
Make ticker not trigger on NYSE holidays
Browse files Browse the repository at this point in the history
  • Loading branch information
ForeverEndeavor authored and johnmaguire committed Jul 30, 2021
1 parent 851e020 commit 083e252
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 2 deletions.
93 changes: 91 additions & 2 deletions plugins/ticker/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,99 @@
from twisted.internet import defer, error, reactor
from twisted.internet.threads import deferToThread

from datetime import date
from dateutil.easter import easter
from dateutil.relativedelta import relativedelta as rd, MO, TH, FR

from holidays.constants import (
JAN,
FEB,
MAY,
JUL,
SEP,
NOV,
DEC,
)
from holidays.constants import FRI, SAT, SUN
from holidays.holiday_base import HolidayBase

from cardinal import util
from cardinal.bot import user_info
from cardinal.decorators import command, help, regex
from cardinal.util import F

# Basis: https://github.com/dr-prodigy/python-holidays/blob/master/holidays/countries/united_states.py
# NYSE currently has nine (9) trading holidays: New Year's Day, MLK Jr. Day,
# Washington's Birthday, Good Friday, Memorial Day, Independence Day,
# Labor Day, Thanksgiving Day, and Christmas Day. This does not take into account
# early closes.
class NYSEHolidays(HolidayBase):

def __init__(self, **kwargs):
self.observed = True
HolidayBase.__init__(self, **kwargs)

def _populate(self, year):
# New Year's Day
name = "New Year's Day"
self[date(year, JAN, 1)] = name
if self.observed and date(year, JAN, 1).weekday() == SUN:
self[date(year, JAN, 1) + rd(days=+1)] = name + " (Observed)"
elif self.observed and date(year, JAN, 1).weekday() == SAT:
# Add Dec 31st from the previous year without triggering
# the entire year to be added
expand = self.expand
self.expand = False
self[date(year, JAN, 1) + rd(days=-1)] = name + " (Observed)"
self.expand = expand
# The next year's observed New Year's Day can be in this year
# when it falls on a Friday (Jan 1st is a Saturday)
if self.observed and date(year, DEC, 31).weekday() == FRI:
self[date(year, DEC, 31)] = name + " (Observed)"

# Martin Luther King Jr. Day
name = "Martin Luther King Jr. Day"
self[date(year, JAN, 1) + rd(weekday=MO(+3))] = name

# Washington's Birthday
name = "Washington's Birthday"
self[date(year, FEB, 1) + rd(weekday=MO(+3))] = name

# Good Friday
name = "Good Friday"
self[easter(year) + rd(weekday=FR(-1))] = name

# Memorial Day
name = "Memorial Day"
self[date(year, MAY, 31) + rd(weekday=MO(-1))] = name

# Independence Day
name = "Independence Day"
self[date(year, JUL, 4)] = name
if self.observed and date(year, JUL, 4).weekday() == SAT:
self[date(year, JUL, 4) + rd(days=-1)] = name + " (Observed)"
elif self.observed and date(year, JUL, 4).weekday() == SUN:
self[date(year, JUL, 4) + rd(days=+1)] = name + " (Observed)"

# Labor Day
name = "Labor Day"
self[date(year, SEP, 1) + rd(weekday=MO)] = name

# Thanksgiving Day
name = "Thanksgiving Day"
self[date(year, NOV, 1) + rd(weekday=TH(+4))] = name

# Christmas Day
name = "Christmas Day"
self[date(year, DEC, 25)] = name
if self.observed and date(year, DEC, 25).weekday() == SAT:
self[date(year, DEC, 25) + rd(days=-1)] = name + " (Observed)"
elif self.observed and date(year, DEC, 25).weekday() == SUN:
self[date(year, DEC, 25) + rd(days=+1)] = name + " (Observed)"

# Class populated with NYSE holidays
HOLIDAYS = NYSEHolidays()

# IEX API Endpoint
IEX_QUOTE_API_URL = "https://cloud.iexapis.com/stable/stock/{symbol}/quote?token={token}" # noqa: E501

Expand All @@ -37,11 +125,11 @@ def est_now():


def market_is_open():
"""Not aware of holidays or anything like that..."""
now = est_now()

# Determine if the market is currently open
is_market_closed = (now.weekday() >= 5) or \
is_market_closed = (now in HOLIDAYS) or \
(now.weekday() >= 5) or \
(now.hour < 9 or now.hour >= 17) or \
(now.hour == 9 and now.minute < 30) or \
(now.hour == 16 and now.minute > 0)
Expand Down Expand Up @@ -146,6 +234,7 @@ def tick(self):

# Determine if the market is currently open
is_market_open = not (
(now in HOLIDAYS) or
(now.weekday() >= 5) or
(now.hour < 9 or now.hour >= 17) or
(now.hour == 9 and now.minute < 30) or
Expand Down
1 change: 1 addition & 0 deletions plugins/ticker/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
holidays==0.11.2
26 changes: 26 additions & 0 deletions plugins/ticker/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from plugins.ticker import plugin
from plugins.ticker.plugin import (
TickerPlugin,
NYSEHolidays,
colorize,
get_delta,
)
Expand Down Expand Up @@ -660,6 +661,7 @@ def test_get_daily(self):
@patch.object(plugin, 'est_now')
def test_market_is_open(self, mock_now):
tz = pytz.timezone('America/New_York')
holidays = NYSEHolidays()

# Nothing special about this time - it's a Thursday 7:49pm
mock_now.return_value = tz.localize(datetime.datetime(
Expand Down Expand Up @@ -708,3 +710,27 @@ def test_market_is_open(self, mock_now):
0,
))
assert plugin.market_is_open() is False

# Or on Memorial Day
mock_now.return_value = tz.localize(datetime.datetime(
2021,
5,
31,
13,
49,
55,
0,
))
assert plugin.market_is_open() is False

# Or on those pesky observed holidays
mock_now.return_value = tz.localize(datetime.datetime(
2021,
7,
5,
13,
49,
55,
0,
))
assert plugin.market_is_open() is False

0 comments on commit 083e252

Please sign in to comment.