diff --git a/tests/finance/test_slippage.py b/tests/finance/test_slippage.py index e8812ec2..378bf15f 100644 --- a/tests/finance/test_slippage.py +++ b/tests/finance/test_slippage.py @@ -26,13 +26,15 @@ import numpy as np import pandas as pd import pytz -from zipline.assets import Equity +from zipline.assets import Equity, Future from zipline.data.data_portal import DataPortal from zipline.finance.asset_restrictions import NoRestrictions from zipline.finance.order import Order from zipline.finance.slippage import ( + EquitySlippageModel, fill_price_worse_than_limit_price, - NO_DATA_VOLATILITY_SLIPPAGE_IMPACT, + FutureSlippageModel, + SlippageModel, VolatilityVolumeShare, VolumeShareSlippage, ) @@ -105,6 +107,47 @@ class SlippageTestCase(WithCreateBarData, self.assertEqual(vol1.__dict__, vol1.asdict()) self.assertEqual(vol2.__dict__, vol2.asdict()) + def test_allowed_asset_types(self): + # Custom equities model. + class MyEquitiesModel(EquitySlippageModel): + def process_order(self, data, order): + return 0, 0 + + self.assertEqual(MyEquitiesModel.allowed_asset_types, (Equity,)) + + # Custom futures model. + class MyFuturesModel(FutureSlippageModel): + def process_order(self, data, order): + return 0, 0 + + self.assertEqual(MyFuturesModel.allowed_asset_types, (Future,)) + + # Custom model for both equities and futures. + class MyMixedModel(EquitySlippageModel, FutureSlippageModel): + def process_order(self, data, order): + return 0, 0 + + self.assertEqual(MyMixedModel.allowed_asset_types, (Equity, Future)) + + # Equivalent custom model for both equities and futures. + class MyMixedModel(SlippageModel): + def process_order(self, data, order): + return 0, 0 + + self.assertEqual(MyMixedModel.allowed_asset_types, (Equity, Future)) + + SomeType = type('SomeType', (object,), {}) + + # A custom model that defines its own allowed types should take + # precedence over the parent class definitions. + class MyCustomModel(EquitySlippageModel, FutureSlippageModel): + allowed_asset_types = (SomeType,) + + def process_order(self, data, order): + return 0, 0 + + self.assertEqual(MyCustomModel.allowed_asset_types, (SomeType,)) + def test_fill_price_worse_than_limit_price(self): non_limit_order = TestOrder(limit=None, direction=1) limit_buy = TestOrder(limit=1.5, direction=1) @@ -725,13 +768,13 @@ class VolatilityVolumeShareTestCase(WithCreateBarData, @classmethod def make_futures_info(cls): return pd.DataFrame({ - 'sid': [1000], - 'root_symbol': ['CL'], - 'symbol': ['CLF07'], - 'start_date': [cls.ASSET_START_DATE], - 'end_date': [cls.END_DATE], - 'multiplier': [500], - 'exchange': ['CME'], + 'sid': [1000, 1001], + 'root_symbol': ['CL', 'FV'], + 'symbol': ['CLF07', 'FVF07'], + 'start_date': [cls.ASSET_START_DATE, cls.START_DATE], + 'end_date': [cls.END_DATE, cls.END_DATE], + 'multiplier': [500, 500], + 'exchange': ['CME', 'CME'], }) @classmethod @@ -799,27 +842,31 @@ class VolatilityVolumeShareTestCase(WithCreateBarData, def test_calculate_impact_without_history(self): model = VolatilityVolumeShare(volume_limit=1) - minutes = [ + late_start_asset = self.asset_finder.retrieve_asset(1000) + early_start_asset = self.asset_finder.retrieve_asset(1001) + + cases = [ + # History will look for data before the start date. + (pd.Timestamp('2006-01-05 11:35AM', tz='UTC'), early_start_asset), # Start day of the futures contract; no history yet. - pd.Timestamp('2006-02-10 11:35AM', tz='UTC'), + (pd.Timestamp('2006-02-10 11:35AM', tz='UTC'), late_start_asset), # Only a week's worth of history data. - pd.Timestamp('2006-02-17 11:35AM', tz='UTC'), + (pd.Timestamp('2006-02-17 11:35AM', tz='UTC'), late_start_asset), ] - for minute in minutes: + for minute, asset in cases: data = self.create_bardata(simulation_dt_func=lambda: minute) - order = Order(dt=data.current_dt, asset=self.ASSET, amount=10) + order = Order(dt=data.current_dt, asset=asset, amount=10) price, amount = model.process_order(data, order) avg_price = ( - data.current(self.ASSET, 'high') + - data.current(self.ASSET, 'low') + data.current(asset, 'high') + data.current(asset, 'low') ) / 2 expected_price = \ - avg_price + (avg_price * NO_DATA_VOLATILITY_SLIPPAGE_IMPACT) + avg_price * (1 + model.NO_DATA_VOLATILITY_SLIPPAGE_IMPACT) - self.assertEqual(price, expected_price) + self.assertAlmostEqual(price, expected_price, delta=0.001) self.assertEqual(amount, 10) def test_impacted_price_worse_than_limit(self): diff --git a/zipline/finance/slippage.py b/zipline/finance/slippage.py index f81a2cb5..bf5484a0 100644 --- a/zipline/finance/slippage.py +++ b/zipline/finance/slippage.py @@ -15,14 +15,16 @@ from __future__ import division from abc import ABCMeta, abstractmethod +from itertools import chain import math -from six import with_metaclass, iteritems -from toolz import merge import numpy as np from pandas import isnull +from six import with_metaclass, iteritems +from toolz import merge from zipline.assets import Equity, Future +from zipline.errors import HistoryWindowStartsBeforeData from zipline.finance.constants import ROOT_SYMBOL_TO_ETA from zipline.finance.transaction import create_transaction from zipline.utils.cache import ExpiringCache @@ -36,8 +38,7 @@ LIMIT = 1 << 3 SQRT_252 = math.sqrt(252) DEFAULT_EQUITY_VOLUME_SLIPPAGE_BAR_LIMIT = 0.025 -DEFAULT_FUTURE_VOLUME_SLIPPAGE_BAR_LIMIT = 0.025 -NO_DATA_VOLATILITY_SLIPPAGE_IMPACT = 10.0 / 10000 +DEFAULT_FUTURE_VOLUME_SLIPPAGE_BAR_LIMIT = 0.05 class LiquidityExceeded(Exception): @@ -77,7 +78,45 @@ def fill_price_worse_than_limit_price(fill_price, order): return False -class SlippageModel(with_metaclass(ABCMeta)): +class SlippageModelMeta(ABCMeta): + """ + This metaclass allows users to create custom slippage models that support + both equities and futures by subclassing EquityFutureModel and + FutureSlippageModel together. + + Take for example the following custom model: + + class MyCustomSlippage(EquitySlippageModel, FutureSlippageModel): + def process_order(self, data, order): + ... + + Ordinarily the first parent class in the MRO ('EquitySlippageModel' in this + example) would override the 'allowed_asset_types' class attribute to only + include equities. However, this is probably not the expected behavior for a + reasonable user, so the metaclass will handle this specific case by + manually allowing both equities and futures for the class being created. + """ + + def __new__(mcls, name, bases, dict_): + if 'allowed_asset_types' not in dict_: + allowed_asset_types = tuple( + chain.from_iterable( + marker.allowed_asset_types + for marker in bases + if isinstance(marker, AssetSlippageMarker) + ) + ) + if allowed_asset_types: + dict_['allowed_asset_types'] = allowed_asset_types + + return super(SlippageModelMeta, mcls).__new__(mcls, name, bases, dict_) + + +class AssetSlippageMarker(SlippageModelMeta): + pass + + +class SlippageModel(with_metaclass(SlippageModelMeta)): """Abstract interface for defining a slippage model. """ @@ -175,14 +214,14 @@ class SlippageModel(with_metaclass(ABCMeta)): return self.__dict__ -class EquitySlippageModel(SlippageModel): +class EquitySlippageModel(with_metaclass(AssetSlippageMarker, SlippageModel)): """ Base class for slippage models which only support equities. """ allowed_asset_types = (Equity,) -class FutureSlippageModel(SlippageModel): +class FutureSlippageModel(with_metaclass(AssetSlippageMarker, SlippageModel)): """ Base class for slippage models which only support futures. """ @@ -293,6 +332,8 @@ class MarketImpactBase(SlippageModel): according to a history lookback. """ + NO_DATA_VOLATILITY_SLIPPAGE_IMPACT = 10.0 / 10000 + def __init__(self): super(MarketImpactBase, self).__init__() self._window_data_cache = ExpiringCache() @@ -365,7 +406,7 @@ class MarketImpactBase(SlippageModel): if mean_volume == 0 or np.isnan(volatility): # If this is the first day the contract exists or there is no # volume history, default to a conservative estimate of impact. - simulated_impact = price * NO_DATA_VOLATILITY_SLIPPAGE_IMPACT + simulated_impact = price * self.NO_DATA_VOLATILITY_SLIPPAGE_IMPACT else: simulated_impact = self.get_simulated_impact( order=order, @@ -404,14 +445,20 @@ class MarketImpactBase(SlippageModel): try: values = self._window_data_cache.get(asset, data.current_session) except KeyError: - # Add a day because we want 'window_length' complete days, - # excluding the current day. - volume_history = data.history( - asset, 'volume', window_length + 1, '1d', - ) - close_history = data.history( - asset, 'close', window_length + 1, '1d', - ) + try: + # Add a day because we want 'window_length' complete days, + # excluding the current day. + volume_history = data.history( + asset, 'volume', window_length + 1, '1d', + ) + close_history = data.history( + asset, 'close', window_length + 1, '1d', + ) + except HistoryWindowStartsBeforeData: + # If there is not enough data to do a full history call, return + # values as if there was no data. + return 0, np.NaN + # Exclude the first value of the percent change array because it is # always just NaN. close_volatility = close_history[:-1].pct_change()[1:].std( @@ -450,6 +497,8 @@ class VolatilityVolumeShare(MarketImpactBase): for all futures contracts is the same. If given a dictionary, it must map root symbols to the eta for contracts of that symbol. """ + + NO_DATA_VOLATILITY_SLIPPAGE_IMPACT = 7.5 / 10000 allowed_asset_types = (Future,) def __init__(self, volume_limit, eta=ROOT_SYMBOL_TO_ETA):