Skip to content

Commit

Permalink
MAINT: Various futures slippage model fixes and cleanup
Browse files Browse the repository at this point in the history
- Handle history lookback error before start date
- Adjust default futures slippage volume limit
- Allow subclassing EquitySlippageModel and FutureSlippageModel together.
  • Loading branch information
dmichalowicz committed May 9, 2017
1 parent e8d60d9 commit a4464e7
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 34 deletions.
83 changes: 65 additions & 18 deletions tests/finance/test_slippage.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@
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,
)
Expand Down Expand Up @@ -105,6 +107,47 @@ def test_equality_and_comparison(self):
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)
Expand Down Expand Up @@ -725,13 +768,13 @@ def init_class_fixtures(cls):
@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
Expand Down Expand Up @@ -799,27 +842,31 @@ def _calculate_impact(self, test_order, answer_key):

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):
Expand Down
81 changes: 65 additions & 16 deletions zipline/finance/slippage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,8 +38,7 @@
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):
Expand Down Expand Up @@ -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.
"""

Expand Down Expand Up @@ -175,14 +214,14 @@ def asdict(self):
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.
"""
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -365,7 +406,7 @@ def process_order(self, data, order):
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,
Expand Down Expand Up @@ -404,14 +445,20 @@ def _get_window_data(self, data, asset, window_length):
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(
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit a4464e7

Please sign in to comment.