diff --git a/tests/test_blotter.py b/tests/finance/test_blotter.py similarity index 86% rename from tests/test_blotter.py rename to tests/finance/test_blotter.py index 21260954..2a031c81 100644 --- a/tests/test_blotter.py +++ b/tests/finance/test_blotter.py @@ -16,22 +16,22 @@ from nose_parameterized import parameterized import pandas as pd +from zipline.assets import Equity from zipline.finance.blotter import Blotter -from zipline.finance.order import ORDER_STATUS, Order +from zipline.finance.cancel_policy import EODCancel, NeverCancel +from zipline.finance.commission import PerTrade from zipline.finance.execution import ( LimitOrder, MarketOrder, StopLimitOrder, StopOrder, ) - -from zipline.gens.sim_engine import SESSION_END, BAR -from zipline.finance.cancel_policy import EODCancel, NeverCancel +from zipline.finance.order import ORDER_STATUS, Order from zipline.finance.slippage import ( DEFAULT_VOLUME_SLIPPAGE_BAR_LIMIT, FixedSlippage, ) -from zipline.utils.classproperty import classproperty +from zipline.gens.sim_engine import BAR, SESSION_END from zipline.testing.fixtures import ( WithCreateBarData, WithDataPortal, @@ -39,6 +39,7 @@ from zipline.testing.fixtures import ( WithSimParams, ZiplineTestCase, ) +from zipline.utils.classproperty import classproperty class BlotterTestCase(WithCreateBarData, @@ -55,6 +56,7 @@ class BlotterTestCase(WithCreateBarData, super(BlotterTestCase, cls).init_class_fixtures() cls.asset_24 = cls.asset_finder.retrieve_asset(24) cls.asset_25 = cls.asset_finder.retrieve_asset(25) + cls.future_cl = cls.asset_finder.retrieve_asset(1000) @classmethod def make_equity_daily_bar_data(cls): @@ -79,6 +81,23 @@ class BlotterTestCase(WithCreateBarData, index=cls.sim_params.sessions, ) + @classmethod + def make_futures_info(cls): + return pd.DataFrame.from_dict( + { + 1000: { + 'symbol': 'CLF06', + 'root_symbol': 'CL', + 'start_date': cls.START_DATE, + 'end_date': cls.END_DATE, + 'expiration_date': cls.END_DATE, + 'auto_close_date': cls.END_DATE, + 'exchange': 'CME', + }, + }, + orient='index', + ) + @classproperty def CREATE_BARDATA_DATA_FREQUENCY(cls): return cls.sim_params.data_frequency @@ -220,7 +239,7 @@ class BlotterTestCase(WithCreateBarData, # Reset for paranoia blotter = Blotter(self.sim_params.data_frequency, self.asset_finder) - blotter.slippage_func = FixedSlippage() + blotter.slippage_models[Equity] = FixedSlippage() filled_id = blotter.order(self.asset_24, 100, MarketOrder()) filled_order = None blotter.current_dt = self.sim_params.sessions[-1] @@ -363,3 +382,40 @@ class BlotterTestCase(WithCreateBarData, blotter1.open_orders[asset][i-1].id) self.assertEqual(order_id, blotter2.open_orders[asset][i-1].id) + + def test_slippage_and_commission_dispatching(self): + blotter = Blotter( + self.sim_params.data_frequency, + self.asset_finder, + equity_slippage=FixedSlippage(spread=0.0), + future_slippage=FixedSlippage(spread=2.0), + equity_commission=PerTrade(cost=1.0), + future_commission=PerTrade(cost=2.0), + ) + blotter.order(self.asset_24, 1, MarketOrder()) + blotter.order(self.future_cl, 1, MarketOrder()) + + bar_data = self.create_bardata( + simulation_dt_func=lambda: self.sim_params.sessions[-1], + ) + txns, commissions, _ = blotter.get_transactions(bar_data) + + # The equity transaction should have the same price as its current + # price because the slippage spread is zero. Its commission should be + # $1.00. + equity_txn = txns[0] + self.assertEqual( + equity_txn.price, + bar_data.current(equity_txn.sid, 'price'), + ) + self.assertEqual(commissions[0]['cost'], 1.0) + + # The future transaction price should be 1.0 more than its current + # price because half of the 'future_slippage' spread is added. Its + # commission should be $2.00. + future_txn = txns[1] + self.assertEqual( + future_txn.price, + bar_data.current(future_txn.sid, 'price') + 1.0, + ) + self.assertEqual(commissions[1]['cost'], 2.0) diff --git a/tests/test_commissions.py b/tests/finance/test_commissions.py similarity index 100% rename from tests/test_commissions.py rename to tests/finance/test_commissions.py diff --git a/tests/test_finance.py b/tests/test_finance.py index bd84ba9b..5b72449f 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -363,7 +363,7 @@ class FinanceTestCase(WithLogger, def test_blotter_processes_splits(self): blotter = Blotter('daily', self.env.asset_finder, - slippage_func=FixedSlippage()) + equity_slippage=FixedSlippage()) # set up two open limit orders with very low limit prices, # one for sid 1 and one for sid 2 diff --git a/zipline/algorithm.py b/zipline/algorithm.py index 74587516..6b438e9e 100644 --- a/zipline/algorithm.py +++ b/zipline/algorithm.py @@ -67,7 +67,7 @@ from zipline.errors import ( ) from zipline.finance.trading import TradingEnvironment from zipline.finance.blotter import Blotter -from zipline.finance.commission import PerShare, CommissionModel +from zipline.finance.commission import CommissionModel from zipline.finance.controls import ( LongOnly, MaxOrderCount, @@ -84,17 +84,14 @@ from zipline.finance.execution import ( ) from zipline.finance.performance import PerformanceTracker from zipline.finance.asset_restrictions import Restrictions -from zipline.finance.slippage import ( - VolumeShareSlippage, - SlippageModel -) +from zipline.finance.slippage import SlippageModel from zipline.finance.cancel_policy import NeverCancel, CancelPolicy from zipline.finance.asset_restrictions import ( NoRestrictions, StaticRestrictions, SecurityListRestrictions, ) -from zipline.assets import Asset, Future +from zipline.assets import Asset, Equity, Future from zipline.gens.tradesimulation import AlgorithmSimulator from zipline.pipeline import Pipeline from zipline.pipeline.engine import ( @@ -324,10 +321,8 @@ class TradingAlgorithm(object): self.blotter = Blotter( data_frequency=self.data_frequency, asset_finder=self.asset_finder, - slippage_func=VolumeShareSlippage(), - commission=PerShare(), # Default to NeverCancel in zipline - cancel_policy=self.cancel_policy + cancel_policy=self.cancel_policy, ) # The symbol lookup date specifies the date to use when resolving @@ -493,16 +488,16 @@ class TradingAlgorithm(object): capital_base={capital_base} sim_params={sim_params}, initialized={initialized}, - slippage={slippage}, - commission={commission}, + slippage_models={slippage_models}, + commission_models={commission_models}, blotter={blotter}, recorded_vars={recorded_vars}) """.strip().format(class_name=self.__class__.__name__, capital_base=self.sim_params.capital_base, sim_params=repr(self.sim_params), initialized=self.initialized, - slippage=repr(self.blotter.slippage_func), - commission=repr(self.blotter.commission), + slippage_models=repr(self.blotter.slippage_models), + commission_models=repr(self.blotter.commission_models), blotter=repr(self.blotter), recorded_vars=repr(self.recorded_vars)) @@ -1662,7 +1657,9 @@ class TradingAlgorithm(object): raise UnsupportedSlippageModel() if self.initialized: raise SetSlippagePostInit() - self.blotter.slippage_func = slippage + # TODO: Create separate API methods for setting Equity and Future + # slippage models. + self.blotter.slippage_models[Equity] = slippage @api_method def set_commission(self, commission): @@ -1685,7 +1682,9 @@ class TradingAlgorithm(object): if self.initialized: raise SetCommissionPostInit() - self.blotter.commission = commission + # TODO: Create separate API methods for setting Equity and Future + # commission models. + self.blotter.commission_models[Equity] = commission @api_method def set_cancel_policy(self, cancel_policy): diff --git a/zipline/finance/blotter.py b/zipline/finance/blotter.py index a73dda0d..9a61e06a 100644 --- a/zipline/finance/blotter.py +++ b/zipline/finance/blotter.py @@ -18,9 +18,14 @@ from copy import copy from six import iteritems +from zipline.assets import Equity, Future from zipline.finance.order import Order from zipline.finance.slippage import VolumeShareSlippage -from zipline.finance.commission import PerShare +from zipline.finance.commission import ( + DEFAULT_FUTURE_COST_PER_TRADE, + PerShare, + PerTrade, +) from zipline.finance.cancel_policy import NeverCancel log = Logger('Blotter') @@ -28,8 +33,9 @@ warning_logger = Logger('AlgoWarning') class Blotter(object): - def __init__(self, data_frequency, asset_finder, slippage_func=None, - commission=None, cancel_policy=None): + def __init__(self, data_frequency, asset_finder, equity_slippage=None, + future_slippage=None, equity_commission=None, + future_commission=None, cancel_policy=None): # these orders are aggregated by sid self.open_orders = defaultdict(list) @@ -47,8 +53,16 @@ class Blotter(object): self.max_shares = int(1e+11) - self.slippage_func = slippage_func or VolumeShareSlippage() - self.commission = commission or PerShare() + self.slippage_models = { + Equity: equity_slippage or VolumeShareSlippage(), + Future: future_slippage or VolumeShareSlippage(), + } + self.commission_models = { + Equity: equity_commission or PerShare(), + Future: future_commission or PerTrade( + cost=DEFAULT_FUTURE_COST_PER_TRADE, + ), + } self.data_frequency = data_frequency @@ -57,15 +71,15 @@ class Blotter(object): def __repr__(self): return """ {class_name}( - slippage={slippage_func}, - commission={commission}, + slippage_models={slippage_models}, + commission_models={commission_models}, open_orders={open_orders}, orders={orders}, new_orders={new_orders}, current_dt={current_dt}) """.strip().format(class_name=self.__class__.__name__, - slippage_func=self.slippage_func, - commission=self.commission, + slippage_models=self.slippage_models, + commission_models=self.commission_models, open_orders=self.open_orders, orders=self.orders, new_orders=self.new_orders, @@ -347,11 +361,12 @@ class Blotter(object): for sid, asset_orders in iteritems(self.open_orders): asset = asset_dict[sid] + slippage = self.slippage_models[type(asset)] for order, txn in \ - self.slippage_func(bar_data, asset, asset_orders): - additional_commission = \ - self.commission.calculate(order, txn) + slippage.simulate(bar_data, asset, asset_orders): + commission = self.commission_models[type(asset)] + additional_commission = commission.calculate(order, txn) if additional_commission > 0: commissions.append({ diff --git a/zipline/finance/commission.py b/zipline/finance/commission.py index a227ef45..60b9cfd8 100644 --- a/zipline/finance/commission.py +++ b/zipline/finance/commission.py @@ -19,6 +19,7 @@ from six import with_metaclass DEFAULT_PER_SHARE_COST = 0.0075 # 0.75 cents per share DEFAULT_MINIMUM_COST_PER_TRADE = 1.0 # $1 per trade +DEFAULT_FUTURE_COST_PER_TRADE = 2.35 class CommissionModel(with_metaclass(abc.ABCMeta)): diff --git a/zipline/finance/slippage.py b/zipline/finance/slippage.py index 42036695..3a35d636 100644 --- a/zipline/finance/slippage.py +++ b/zipline/finance/slippage.py @@ -149,9 +149,6 @@ class SlippageModel(with_metaclass(abc.ABCMeta)): self._volume_for_bar += abs(txn.amount) yield order, txn - def __call__(self, bar_data, asset, current_orders): - return self.simulate(bar_data, asset, current_orders) - def __eq__(self, other): return self.asdict() == other.asdict()