MAINT: Consolidates minute_window methods in schedule classes

This commit is contained in:
jfkirk
2016-04-20 11:08:34 -04:00
committed by Jean Bredeche
parent 26742dda67
commit ddaf3d5b02
8 changed files with 118 additions and 256 deletions
+1 -1
View File
@@ -112,7 +112,7 @@ class ExchangeCalendarTestBase(object):
def test_minute_window(self):
for open in self.answers.market_open:
open_tz = open.tz_localize('UTC')
window = self.calendar.minute_window(open_tz, 390, 1)
window = self.calendar.market_minute_window(open_tz, 390, step=1)
self.assertEqual(len(window), 390)
+11 -11
View File
@@ -203,7 +203,7 @@ class FinanceTestCase(WithLogger,
data_frequency="minute"
)
minutes = default_nyse_schedule.minute_window(
minutes = default_nyse_schedule.execution_minute_window(
sim_params.first_open,
int((trade_interval.total_seconds() / 60) * trade_count)
+ 100)
@@ -497,7 +497,7 @@ class TradingEnvironmentTestCase(WithLogger,
utc_start = pd.Timestamp(start.astimezone(utc))
# Get the next 10 minutes
minutes = self.cal.minute_window(
minutes = self.cal.market_minute_window(
utc_start, 10,
)
self.assertEqual(len(minutes), 10)
@@ -505,7 +505,7 @@ class TradingEnvironmentTestCase(WithLogger,
self.assertEqual(minutes[i], utc_start + timedelta(minutes=i))
# Get the previous 10 minutes.
minutes = self.cal.minute_window(
minutes = self.cal.market_minute_window(
utc_start, 10, step=-1,
)
self.assertEqual(len(minutes), 10)
@@ -518,14 +518,14 @@ class TradingEnvironmentTestCase(WithLogger,
# Today: 10:01 AM -> 4:00 PM (360 minutes)
# Tomorrow: 9:31 AM -> 4:00 PM (390 minutes, 750 total)
# Last Day: 9:31 AM -> 12:00 PM (150 minutes, 900 total)
minutes = self.cal.minute_window(
minutes = self.cal.market_minute_window(
start, 900,
)
today = self.cal.minutes_for_date(utc_start)[30:]
tomorrow = self.cal.minutes_for_date(
today = self.cal.trading_minutes_for_day(utc_start)[30:]
tomorrow = self.cal.trading_minutes_for_day(
start + timedelta(days=1)
)
last_day = self.cal.minutes_for_date(
last_day = self.cal.trading_minutes_for_day(
start + timedelta(days=2))[:150]
self.assertEqual(len(minutes), 900)
@@ -540,17 +540,17 @@ class TradingEnvironmentTestCase(WithLogger,
# Today: 10:01 AM -> 9:31 AM (31 minutes)
# Friday: 4:00 PM -> 9:31 AM (390 minutes, 421 total)
# Thursday: 4:00 PM -> 9:41 AM (380 minutes, 801 total)
minutes = self.cal.minute_window(
minutes = self.cal.market_minute_window(
start, 801, step=-1,
)
today = self.cal.minutes_for_date(utc_start)[30::-1]
today = self.cal.trading_minutes_for_day(utc_start)[30::-1]
# minus an extra two days from each of these to account for the two
# weekend days we skipped
friday = self.cal.minutes_for_date(
friday = self.cal.trading_minutes_for_day(
start + timedelta(days=-3),
)[::-1]
thursday = self.cal.minutes_for_date(
thursday = self.cal.trading_minutes_for_day(
start + timedelta(days=-4),
)[:9:-1]
+62 -2
View File
@@ -16,6 +16,7 @@
import pandas as pd
import numpy as np
import bisect
from pytz import timezone
from zipline.errors import NoFurtherDataError
@@ -110,11 +111,13 @@ def days_in_range(start, end, all_days):
Get all execution days between start and end,
inclusive.
"""
start_date = normalize_date(start)
end_date = normalize_date(end)
return all_days[all_days.slice_indexer(start_date, end_date)]
mask = ((all_days >= start_date) & (all_days <= end_date))
return all_days[mask]
#mask = ((all_days >= start_date) & (all_days <= end_date))
#return all_days[mask]
def minutes_for_days_in_range(start, end, days_in_range_hook,
@@ -211,3 +214,60 @@ def previous_scheduled_minute(start, is_scheduled_day_hook,
# If start is not a trading day, or is before the market open
# then return the close of the *previous* trading day.
return previous_open_and_close_hook(start)[1]
def minute_window(start, count, schedule, is_scheduled_minute_hook,
session_date_hook, minutes_for_date_hook, step=1):
"""
Returns a DatetimeIndex containing `count` market minutes, starting
with `start` and continuing `step` minutes at a time.
Parameters
----------
start : Timestamp
The start of the window.
count : int
The number of minutes needed.
step : int
The step size by which to increment.
Returns
-------
DatetimeIndex
A window with @count minutes, start with @start.
"""
if not is_scheduled_minute_hook(start):
raise ValueError("minute_window starting at non-market time "
"{minute}".format(minute=start))
start_utc = start.astimezone(timezone('UTC'))
session = session_date_hook(start)
session_idx = schedule.index.get_loc(session)
mins_in_session = minutes_for_date_hook(session)
start_idx = mins_in_session.searchsorted(start_utc)
# Use a list instead of a pandas DatetimeIndex, as using .append()
# with DatetimeIndex can become expensive if used several times, since
# it makes a full copy of the data. list.extend() will not typically
# copy the data unless there is not enough memory to extend into, which
# is usually not problem.
all_minutes = list(mins_in_session[start_idx::np.sign(step)])
while True:
step_minutes = all_minutes[0::np.absolute(step)]
if len(step_minutes) >= count:
step_minutes = step_minutes[:count]
return pd.DatetimeIndex(step_minutes, copy=False)
# Iterate session forward or backward
session_idx += np.sign(step)
# Get the minutes in the next exchange session
session = schedule.index[session_idx]
session_minutes = minutes_for_date_hook(session)[::np.sign(step)]
# A these new session_minutes to the `all_minutes` candidate list
all_minutes.extend(list(session_minutes))
@@ -374,81 +374,3 @@ class CMEExchangeCalendar(ExchangeCalendar):
"""
dt_utc = dt.tz_convert('UTC').tz_convert(None)
return dt_utc.replace(hour=0, minute=0, second=0)
def minutes_for_date(self, date):
"""
Given a UTC-canonicalized date, returns a DatetimeIndex of all trading
minutes in the exchange session for that date.
SD: Sounds like @date can be an arbitrary datetime, and that we should
first map to an exchange session by calling self.session_date. Need to
check what the consumers expect.
Parameters
----------
date : Timestamp
The UTC-canonicalized date whose minutes are needed.
Returns
-------
DatetimeIndex
A DatetimeIndex populated with all of the minutes in the
given date.
"""
open, close = self.open_and_close(date)
return date_range(open, close, freq='min', tz='UTC')
def minute_window(self, start, count, step=1):
"""
Return a DatetimeIndex containing `count` market minutes, starting with
`start` and continuing `step` minutes at a time.
Parameters
----------
start : Timestamp
The start of the window.
count : int
The number of minutes needed.
step : int
The step size by which to increment.
Returns
-------
DatetimeIndex
A window with @count minutes, start with @start.
"""
if not self.is_open(start):
raise ValueError("minute_window starting at non-market time "
"{minute}".format(minute=start))
start_utc = start.tz_convert('UTC')
session = self.session_date(start)
session_idx = self.schedule.index.get_loc(session)
mins_in_session = self.minutes_for_date(session)
start_idx = mins_in_session.searchsorted(start_utc)
# Use a list instead of a pandas DatetimeIndex, as using .append()
# with DatetimeIndex can become expensive if used several times, since
# it makes a full copy of the data. list.extend() will not typically
# copy the data unless there is not enough memory to extend into, which
# is usually not problem.
all_minutes = list(mins_in_session[start_idx::np.sign(step)])
while True:
step_minutes = all_minutes[0::step]
if len(step_minutes) >= count:
step_minutes = step_minutes[:count]
return pd.DatetimeIndex(step_minutes, copy=False)
# Iterate session forward or backward
session_idx += np.sign(step)
# Get the minutes in the next exchange session
session = self.schedule.index[session_idx]
session_minutes = self.minutes_for_date(session)
# A these new session_minutes to the `all_minutes` candidate list
all_minutes.extend(list(session_minutes))
+16 -48
View File
@@ -48,6 +48,7 @@ from .calendar_helpers import (
all_scheduled_minutes,
next_scheduled_minute,
previous_scheduled_minute,
minute_window,
)
start_default = pd.Timestamp('1990-01-01', tz='UTC')
@@ -242,6 +243,13 @@ class ExchangeCalendar(with_metaclass(ABCMeta)):
open_and_close_hook=self.open_and_close,
previous_open_and_close_hook=self.previous_open_and_close,
)
self.market_minute_window = partial(
minute_window,
schedule=self.schedule,
is_scheduled_minute_hook=self.is_open_on_minute,
session_date_hook=self.session_date,
minutes_for_date_hook=self.trading_minutes_for_day,
)
def _special_dates(self, calendars, ad_hoc_dates, start_date, end_date):
"""
@@ -413,56 +421,10 @@ class ExchangeCalendar(with_metaclass(ABCMeta)):
"""
raise NotImplementedError()
@abstractmethod
def minutes_for_date(self, date):
"""
Given a UTC-canonicalized date, returns a DatetimeIndex of all trading
minutes in the exchange session for that date.
SD: Sounds like @date can be an arbitrary datetime, and that we should
first map to an exchange session by calling self.session_date. Need to
check what the consumers expect.
Parameters
----------
date : Timestamp
The UTC-canonicalized date whose minutes are needed.
Returns
-------
DatetimeIndex
A DatetimeIndex populated with all of the minutes in the
given date.
"""
raise NotImplementedError()
@abstractmethod
def minute_window(self, start, count, step=1):
"""
Return a DatetimeIndex containing `count` market minutes, starting with
`start` and continuing `step` minutes at a time.
Parameters
----------
start : Timestamp
The start of the window.
count : int
The number of minutes needed.
step : int
The step size by which to increment.
Returns
-------
DatetimeIndex
A window with @count minutes, starting with @start a returning
every @step minute.
"""
raise NotImplementedError()
_static_calendars = {}
_lazy_calendar_names = ['NYSE']
_lazy_calendar_names = ['NYSE', 'CME']
def get_calendar(name):
@@ -481,12 +443,18 @@ def get_calendar(name):
# It's not a lazy calendar, so raise an exception
raise InvalidCalendarName(calendar_name=name)
if name is 'NYSE':
if name == 'NYSE':
from zipline.utils.calendars.nyse_exchange_calendar \
import NYSEExchangeCalendar
nyse_cal = NYSEExchangeCalendar()
register_calendar(nyse_cal)
if name == 'CME':
from zipline.utils.calendars.cme_exchange_calendar \
import CMEExchangeCalendar
cme_cal = CMEExchangeCalendar()
register_calendar(cme_cal)
return _static_calendars[name]
@@ -16,8 +16,6 @@
from datetime import time
from itertools import chain
import numpy as np
import pandas as pd
from dateutil.relativedelta import (
MO,
TH,
@@ -440,83 +438,3 @@ class NYSEExchangeCalendar(ExchangeCalendar):
while not self.is_open_on_day(dt):
dt += Timedelta(days=1)
return normalize_date(dt)
def minutes_for_date(self, dt):
"""
Given a datetime, returns a DatetimeIndex of all trading
minutes in the exchange session for that datetime.
SD: Should @dt be an arbitrary datetime, so that we should
first map to an exchange session by calling self.session_date. Need to
check what the consumers expect. Here, I assume we need to map it to a
session.
Parameters
----------
dt : Timestamp
The datetime whose exchange session minutes are needed.
Returns
-------
DatetimeIndex
A DatetimeIndex populated with all of the minutes in the
given dt.
"""
session = self.session_date(dt)
open, close = self.open_and_close(session)
return date_range(open, close, freq='min', tz='UTC')
def minute_window(self, start, count, step=1):
"""
Returns a DatetimeIndex containing `count` market minutes, starting
with `start` and continuing `step` minutes at a time.
Parameters
----------
start : Timestamp
The start of the window.
count : int
The number of minutes needed.
step : int
The step size by which to increment.
Returns
-------
DatetimeIndex
A window with @count minutes, start with @start.
"""
if not self.is_open_on_minute(start):
raise ValueError("minute_window starting at non-market time "
"{minute}".format(minute=start))
start_utc = start.astimezone(timezone('UTC'))
session = self.session_date(start)
session_idx = self.schedule.index.get_loc(session)
mins_in_session = self.minutes_for_date(session)
start_idx = mins_in_session.searchsorted(start_utc)
# Use a list instead of a pandas DatetimeIndex, as using .append()
# with DatetimeIndex can become expensive if used several times, since
# it makes a full copy of the data. list.extend() will not typically
# copy the data unless there is not enough memory to extend into, which
# is usually not problem.
all_minutes = list(mins_in_session[start_idx::np.sign(step)])
while True:
step_minutes = all_minutes[0::np.absolute(step)]
if len(step_minutes) >= count:
step_minutes = step_minutes[:count]
return pd.DatetimeIndex(step_minutes, copy=False)
# Iterate session forward or backward
session_idx += np.sign(step)
# Get the minutes in the next exchange session
session = self.schedule.index[session_idx]
session_minutes = self.minutes_for_date(session)[::np.sign(step)]
# A these new session_minutes to the `all_minutes` candidate list
all_minutes.extend(list(session_minutes))
+25 -19
View File
@@ -19,6 +19,7 @@ from abc import (
abstractproperty,
)
from functools import partial
from six import with_metaclass
from zipline.utils.memoize import remember_last
@@ -36,14 +37,14 @@ from .calendar_helpers import (
all_scheduled_minutes,
next_scheduled_minute,
previous_scheduled_minute,
minute_window,
)
class TradingSchedule(object):
class TradingSchedule(with_metaclass(ABCMeta)):
"""
A TradingSchedule defines the execution timing of a TradingAlgorithm.
"""
__metaclass__ = ABCMeta
def __init__(self):
# Assign the partial calendar helpers
@@ -102,12 +103,19 @@ class TradingSchedule(object):
open_and_close_hook=self.start_and_end,
previous_open_and_close_hook=self.previous_start_and_end,
)
self.execution_minute_window = partial(
minute_window,
schedule=self.schedule,
is_scheduled_minute_hook=self.is_executing_on_minute,
session_date_hook=self.session_date,
minutes_for_date_hook=self.execution_minutes_for_day,
)
@abstractproperty
def day(self):
"""
A CustomBusinessDay defining those days on which the algorithm is
usually trading.
trading.
"""
raise NotImplementedError()
@@ -257,26 +265,23 @@ class TradingSchedule(object):
"""
raise NotImplementedError()
@abstractmethod
def minute_window(self, start, count, step=1):
def session_date(self, dt):
"""
Return a DatetimeIndex containing `count` market minutes, starting with
`start` and continuing `step` minutes at a time.
Given a time, returns the UTC-canonicalized date of the trading
session in which the time belongs. If the time is not in a trading
session (while algorithm isn't trading), returns the date of the next
exchange session after the time.
Parameters
----------
start : Timestamp
The start of the window.
count : int
The number of minutes needed.
step : int
The step size by which to increment.
dt : Timestamp
Returns
-------
DatetimeIndex
A window with @count minutes, starting with @start a returning
every @step minute.
Timestamp
The date of the exchange session in which dt belongs.
"""
raise NotImplementedError()
@@ -358,10 +363,11 @@ class ExchangeTradingSchedule(TradingSchedule):
"""
return self._exchange_calendar.is_open_on_day(dt)
def minute_window(self, start, count, step=1):
return self._exchange_calendar.minute_window(start=start,
count=count,
step=step)
def session_date(self, dt):
"""
See TradingSchedule definition.
"""
return self._exchange_calendar.session_date(dt)
@property
def early_ends(self):
+3 -15
View File
@@ -38,21 +38,9 @@ def create_test_zipline(**config):
"argument 'sid_list' or 'sid'")
concurrent_trades = config.get('concurrent_trades', False)
if 'order_count' in config:
order_count = config['order_count']
else:
order_count = 100
if 'order_amount' in config:
order_amount = config['order_amount']
else:
order_amount = 100
if 'trading_schedule' in config:
trading_schedule = config['trading_schedule']
else:
trading_schedule = default_nyse_schedule
order_count = config.get('order_count', 100)
order_amount = config.get('order_amount', 100)
trading_schedule = config.get('trading_schedule', default_nyse_schedule)
# -------------------
# Create the Algo