mirror of
https://github.com/wassname/catalyst.git
synced 2026-08-11 11:16:15 +08:00
Moved grouping by date earlier in the pipeline of generators, prior to any date-dependent state getting involved. Grouping pulls from the pipeline until the start of the next group, which is in the next day. The effect of grouping after slippage but before handle_data is that slippage and the algo are out of sync by a transaction.
199 lines
6.1 KiB
Python
199 lines
6.1 KiB
Python
#
|
|
# Copyright 2012 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 pytz
|
|
import logbook
|
|
import datetime
|
|
|
|
from collections import defaultdict
|
|
|
|
import zipline.protocol as zp
|
|
from zipline.finance.slippage import (
|
|
VolumeShareSlippage,
|
|
transact_partial
|
|
)
|
|
from zipline.finance.commission import PerShare
|
|
|
|
log = logbook.Logger('Transaction Simulator')
|
|
|
|
|
|
class TransactionSimulator(object):
|
|
|
|
def __init__(self):
|
|
self.transact = transact_partial(VolumeShareSlippage(), PerShare())
|
|
self.open_orders = defaultdict(list)
|
|
|
|
def place_order(self, order):
|
|
# initialized filled field.
|
|
order.filled = 0
|
|
self.open_orders[order.sid].append(order)
|
|
|
|
def transform(self, stream_in):
|
|
"""
|
|
Main generator work loop.
|
|
"""
|
|
for date, snapshot in stream_in:
|
|
yield date, [self.update(event) for event in snapshot]
|
|
|
|
def update(self, event):
|
|
event.TRANSACTION = None
|
|
# We only fill transactions on trade events.
|
|
if event.type == zp.DATASOURCE_TYPE.TRADE:
|
|
event.TRANSACTION = self.transact(event, self.open_orders)
|
|
return event
|
|
|
|
|
|
class TradingEnvironment(object):
|
|
|
|
def __init__(
|
|
self,
|
|
benchmark_returns,
|
|
treasury_curves,
|
|
period_start=None,
|
|
period_end=None,
|
|
capital_base=None
|
|
):
|
|
|
|
self.trading_days = []
|
|
self.trading_day_map = {}
|
|
self.treasury_curves = treasury_curves
|
|
self.benchmark_returns = benchmark_returns
|
|
self.period_start = period_start
|
|
self.period_end = period_end
|
|
self.capital_base = capital_base
|
|
self.period_trading_days = None
|
|
|
|
assert self.period_start <= self.period_end, \
|
|
"Period start falls after period end."
|
|
|
|
for bm in benchmark_returns:
|
|
self.trading_days.append(bm.date)
|
|
self.trading_day_map[bm.date] = bm
|
|
|
|
assert self.period_start <= self.trading_days[-1], \
|
|
"Period start falls after the last known trading day."
|
|
assert self.period_end >= self.trading_days[0], \
|
|
"Period end falls before the first known trading day."
|
|
|
|
self.first_open = self.calculate_first_open()
|
|
self.last_close = self.calculate_last_close()
|
|
|
|
self.prior_day_open = self.calculate_prior_day_open()
|
|
|
|
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 self.is_trading_day(first_open):
|
|
first_open = first_open + one_day
|
|
|
|
first_open = self.set_NYSE_time(first_open, 9, 30)
|
|
return first_open
|
|
|
|
def calculate_prior_day_open(self):
|
|
"""
|
|
Finds the first trading day open that falls at least a day
|
|
before period_start.
|
|
"""
|
|
one_day = datetime.timedelta(days=1)
|
|
first_open = self.period_start - one_day
|
|
|
|
if first_open <= self.trading_days[0]:
|
|
log.warn("Cannot calculate prior day open.")
|
|
return self.period_start
|
|
|
|
while not self.is_trading_day(first_open):
|
|
first_open = first_open - one_day
|
|
|
|
first_open = self.set_NYSE_time(first_open, 9, 30)
|
|
return first_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 self.is_trading_day(last_close):
|
|
last_close = last_close - one_day
|
|
|
|
last_close = self.set_NYSE_time(last_close, 16, 00)
|
|
|
|
return last_close
|
|
|
|
#TODO: add other exchanges and timezones...
|
|
def set_NYSE_time(self, dt, hour, minute):
|
|
naive = datetime.datetime(
|
|
year=dt.year,
|
|
month=dt.month,
|
|
day=dt.day
|
|
)
|
|
local = pytz.timezone('US/Eastern')
|
|
local_dt = naive.replace(tzinfo=local)
|
|
# set the clock to the opening bell in NYC time.
|
|
local_dt = local_dt.replace(hour=hour, minute=minute)
|
|
# convert to UTC
|
|
utc_dt = local_dt.astimezone(pytz.utc)
|
|
return utc_dt
|
|
|
|
def normalize_date(self, test_date):
|
|
return datetime.datetime(
|
|
year=test_date.year,
|
|
month=test_date.month,
|
|
day=test_date.day,
|
|
tzinfo=pytz.utc
|
|
)
|
|
|
|
@property
|
|
def days_in_period(self):
|
|
"""return the number of trading days within the period [start, end)"""
|
|
assert self.period_start is not None
|
|
assert self.period_end is not None
|
|
|
|
if self.period_trading_days is None:
|
|
self.period_trading_days = []
|
|
for date in self.trading_days:
|
|
if date > self.period_end:
|
|
break
|
|
if date >= self.period_start:
|
|
self.period_trading_days.append(date)
|
|
|
|
return len(self.period_trading_days)
|
|
|
|
def is_market_hours(self, test_date):
|
|
if not self.is_trading_day(test_date):
|
|
return False
|
|
|
|
mkt_open = self.set_NYSE_time(test_date, 9, 30)
|
|
#TODO: half days?
|
|
mkt_close = self.set_NYSE_time(test_date, 16, 00)
|
|
|
|
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 get_benchmark_daily_return(self, test_date):
|
|
date = self.normalize_date(test_date)
|
|
if date in self.trading_day_map:
|
|
return self.trading_day_map[date].returns
|
|
else:
|
|
return 0.0
|