mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-06 04:10:16 +08:00
ab1a23526d
market_open member does not exist on TradingEnvironment.
318 lines
10 KiB
Python
318 lines
10 KiB
Python
#
|
|
# Copyright 2013 Quantopian, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import bisect
|
|
import pytz
|
|
import logbook
|
|
import datetime
|
|
|
|
from functools import wraps
|
|
from delorean import Delorean
|
|
import pandas as pd
|
|
from pandas import DatetimeIndex
|
|
|
|
from collections import OrderedDict
|
|
from zipline.data.loader import load_market_data
|
|
|
|
|
|
log = logbook.Logger('Transaction Simulator')
|
|
|
|
|
|
# The financial simulations in zipline depend on information
|
|
# about the benchmark index and the risk free rates of return.
|
|
# The benchmark index defines the benchmark returns used in
|
|
# the calculation of performance metrics such as alpha/beta. Many
|
|
# components, including risk, performance, transforms, and
|
|
# batch_transforms, need access to a calendar of trading days and
|
|
# market hours. The TradingEnvironment maintains two time keeping
|
|
# facilities:
|
|
# - a DatetimeIndex of trading days for calendar calculations
|
|
# - a timezone name, which should be local to the exchange
|
|
# hosting the benchmark index. All dates are normalized to UTC
|
|
# for serialization and storage, and the timezone is used to
|
|
# ensure proper rollover through daylight savings and so on.
|
|
#
|
|
# This module maintains a global variable, environment, which is
|
|
# subsequently referenced directly by zipline financial
|
|
# components. To set the environment, you can set the property on
|
|
# the module directly:
|
|
# import zipline.finance.trading as trading
|
|
# trading.environment = TradingEnvironment()
|
|
#
|
|
# or if you want to switch the environment for a limited context
|
|
# you can use a TradingEnvironment in a with clause:
|
|
# lse = TradingEnvironment(bm_index="^FTSE", exchange_tz="Europe/London")
|
|
# with lse:
|
|
# # the code here will have lse as the global trading.environment
|
|
# algo.run(start, end)
|
|
#
|
|
# User code will not normally need to use TradingEnvironment
|
|
# directly. If you are extending zipline's core financial
|
|
# compponents and need to use the environment, you must import the module
|
|
# NOT the variable. If you import the module, you will get a
|
|
# reference to the environment at import time, which will prevent
|
|
# your code from responding to user code that changes the global
|
|
# state.
|
|
|
|
environment = None
|
|
|
|
|
|
class TradingEnvironment(object):
|
|
|
|
def __init__(
|
|
self,
|
|
load=None,
|
|
bm_symbol='^GSPC',
|
|
exchange_tz="US/Eastern"
|
|
):
|
|
self.prev_environment = self
|
|
self.trading_day_map = OrderedDict()
|
|
self.bm_symbol = bm_symbol
|
|
if not load:
|
|
load = load_market_data
|
|
|
|
self.benchmark_returns, treasury_curves_map = \
|
|
load(self.bm_symbol)
|
|
|
|
self.treasury_curves = pd.Series(treasury_curves_map)
|
|
|
|
self._period_trading_days = None
|
|
self._trading_days_series = None
|
|
self.full_trading_day = datetime.timedelta(hours=6, minutes=30)
|
|
self.exchange_tz = exchange_tz
|
|
|
|
for bm in self.benchmark_returns:
|
|
self.trading_day_map[bm.date] = bm
|
|
|
|
self.first_trading_day = next(self.trading_day_map.iterkeys())
|
|
self.last_trading_day = next(reversed(self.trading_day_map))
|
|
|
|
def __enter__(self, *args, **kwargs):
|
|
global environment
|
|
self.prev_environment = environment
|
|
environment = self
|
|
# return value here is associated with "as such_and_such" on the
|
|
# with clause.
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
global environment
|
|
environment = self.prev_environment
|
|
# signal that any exceptions need to be propagated up the
|
|
# stack.
|
|
return False
|
|
|
|
def normalize_date(self, test_date):
|
|
return datetime.datetime(
|
|
year=test_date.year,
|
|
month=test_date.month,
|
|
day=test_date.day,
|
|
tzinfo=pytz.utc
|
|
)
|
|
|
|
def exchange_dt_in_utc(self, dt):
|
|
delorean = Delorean(dt, self.exchange_tz)
|
|
return delorean.shift(pytz.utc.zone).datetime
|
|
|
|
@property
|
|
def period_trading_days(self):
|
|
if self._period_trading_days is None:
|
|
self._period_trading_days = []
|
|
for date in self.trading_day_map.iterkeys():
|
|
if date > self.period_end:
|
|
break
|
|
if date >= self.period_start:
|
|
self.period_trading_days.append(date)
|
|
return self._period_trading_days
|
|
|
|
@property
|
|
def trading_days(self):
|
|
if self._trading_days_series is None:
|
|
self._trading_days_series = \
|
|
DatetimeIndex(self.trading_day_map.iterkeys())
|
|
return self._trading_days_series
|
|
|
|
def is_market_hours(self, test_date):
|
|
if not self.is_trading_day(test_date):
|
|
return False
|
|
|
|
mkt_open, mkt_close = self.get_open_and_close(test_date)
|
|
return test_date >= mkt_open and test_date <= mkt_close
|
|
|
|
def is_trading_day(self, test_date):
|
|
dt = self.normalize_date(test_date)
|
|
return (dt in self.trading_day_map)
|
|
|
|
def next_trading_day(self, test_date):
|
|
dt = self.normalize_date(test_date)
|
|
delta = datetime.timedelta(days=1)
|
|
|
|
while dt <= self.last_trading_day:
|
|
dt += delta
|
|
if dt in self.trading_day_map:
|
|
return dt
|
|
|
|
return None
|
|
|
|
def next_open_and_close(self, start_date):
|
|
"""
|
|
Given the start_date, returns the next open and close of
|
|
the market.
|
|
"""
|
|
next_open = self.next_trading_day(start_date)
|
|
|
|
if next_open is None:
|
|
raise Exception(
|
|
"Attempt to backtest beyond available history. \
|
|
Last successful date: %s" % self.last_trading_day)
|
|
|
|
return self.get_open_and_close(next_open)
|
|
|
|
def get_open_and_close(self, next_open):
|
|
|
|
# creating a naive datetime with the correct hour,
|
|
# minute, and date. this will allow us to use Delorean to
|
|
# shift the time between EST and UTC.
|
|
next_open = next_open.replace(
|
|
hour=9,
|
|
minute=31,
|
|
second=0,
|
|
microsecond=0,
|
|
tzinfo=None
|
|
)
|
|
# create a new Delorean with the next_open naive date and
|
|
# the correct timezone for the exchange.
|
|
open_utc = self.exchange_dt_in_utc(next_open)
|
|
|
|
market_open = open_utc
|
|
market_close = (market_open
|
|
+ self.get_trading_day_duration(open_utc)
|
|
- datetime.timedelta(minutes=1))
|
|
|
|
return market_open, market_close
|
|
|
|
def get_trading_day_duration(self, trading_day):
|
|
# TODO: make a list of half-days and modify the
|
|
# calculation of market close to reflect them.
|
|
return self.full_trading_day
|
|
|
|
def trading_day_distance(self, first_date, second_date):
|
|
first_date = self.normalize_date(first_date)
|
|
second_date = self.normalize_date(second_date)
|
|
|
|
trading_days = self.trading_day_map.keys()
|
|
# Find leftmost item greater than or equal to day
|
|
i = bisect.bisect_left(trading_days, first_date)
|
|
if i == len(trading_days): # nothing found
|
|
return None
|
|
j = bisect.bisect_left(trading_days, second_date)
|
|
if j == len(trading_days):
|
|
return None
|
|
|
|
return j - i
|
|
|
|
def get_index(self, dt):
|
|
ndt = self.normalize_date(dt)
|
|
return self.trading_days.searchsorted(ndt)
|
|
|
|
|
|
class SimulationParameters(object):
|
|
def __init__(self, period_start, period_end,
|
|
capital_base=10e3,
|
|
emission_rate='daily'):
|
|
|
|
global environment
|
|
if not environment:
|
|
# This is the global environment for trading simulation.
|
|
environment = TradingEnvironment()
|
|
|
|
self.period_start = period_start
|
|
self.period_end = period_end
|
|
self.capital_base = capital_base
|
|
|
|
self.emission_rate = emission_rate
|
|
|
|
assert self.period_start <= self.period_end, \
|
|
"Period start falls after period end."
|
|
|
|
assert self.period_start <= environment.last_trading_day, \
|
|
"Period start falls after the last known trading day."
|
|
assert self.period_end >= environment.first_trading_day, \
|
|
"Period end falls before the first known trading day."
|
|
|
|
self.first_open = self.calculate_first_open()
|
|
self.last_close = self.calculate_last_close()
|
|
start_index = \
|
|
environment.get_index(self.first_open)
|
|
end_index = environment.get_index(self.last_close)
|
|
|
|
# take an inclusive slice of the environment's
|
|
# trading_days.
|
|
self.trading_days = \
|
|
environment.trading_days[start_index:end_index + 1]
|
|
|
|
def calculate_first_open(self):
|
|
"""
|
|
Finds the first trading day on or after self.period_start.
|
|
"""
|
|
first_open = self.period_start
|
|
one_day = datetime.timedelta(days=1)
|
|
|
|
while not environment.is_trading_day(first_open):
|
|
first_open = first_open + one_day
|
|
|
|
mkt_open, _ = environment.get_open_and_close(first_open)
|
|
return mkt_open
|
|
|
|
def calculate_last_close(self):
|
|
"""
|
|
Finds the last trading day on or before self.period_end
|
|
"""
|
|
last_close = self.period_end
|
|
one_day = datetime.timedelta(days=1)
|
|
|
|
while not environment.is_trading_day(last_close):
|
|
last_close = last_close - one_day
|
|
|
|
_, mkt_close = environment.get_open_and_close(last_close)
|
|
return mkt_close
|
|
|
|
@property
|
|
def days_in_period(self):
|
|
"""return the number of trading days within the period [start, end)"""
|
|
return len(self.trading_days)
|
|
|
|
def __repr__(self):
|
|
return "%s(%r)" % (
|
|
self.__class__.__name__,
|
|
{'first_open': self.first_open,
|
|
'last_close': self.last_close
|
|
})
|
|
|
|
|
|
class use_environment(object):
|
|
"""A decorator to wrap a method in a particular
|
|
trading environment."""
|
|
|
|
def __init__(self, environment):
|
|
self.env = environment
|
|
|
|
def __call__(self, func):
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
with self.env:
|
|
return func(*args, **kwargs)
|
|
return wrapper
|