From e5a137f205fc55760832abd5db3fb906b27ffb44 Mon Sep 17 00:00:00 2001 From: Andrew Campbell Date: Fri, 21 Jul 2017 18:05:32 -0500 Subject: [PATCH] ENH: Bound trade amount with asset specific min trade size --- catalyst/algorithm.py | 17 +++++++---------- catalyst/assets/_assets.pyx | 15 +++++++++++---- catalyst/assets/asset_db_schema.py | 3 ++- catalyst/assets/asset_writer.py | 1 + catalyst/finance/slippage.py | 7 +++++-- catalyst/finance/transaction.py | 6 +----- catalyst/utils/math_utils.py | 2 ++ 7 files changed, 29 insertions(+), 22 deletions(-) diff --git a/catalyst/algorithm.py b/catalyst/algorithm.py index ed10fedd..5601f133 100644 --- a/catalyst/algorithm.py +++ b/catalyst/algorithm.py @@ -125,6 +125,7 @@ from catalyst.utils.factory import create_simulation_parameters from catalyst.utils.math_utils import ( tolerant_equals, round_if_near_integer, + round_nearest ) from catalyst.utils.pandas_utils import clear_dataframe_indexer_caches from catalyst.utils.preprocess import preprocess @@ -1488,7 +1489,7 @@ class TradingAlgorithm(object): def _calculate_order(self, asset, amount, limit_price=None, stop_price=None, style=None): - amount = self.round_order(amount) + amount = self.round_order(amount, asset) # Raises a ZiplineError if invalid parameters are detected. self.validate_order_params(asset, @@ -1505,16 +1506,13 @@ class TradingAlgorithm(object): return amount, style @staticmethod - def round_order(amount): + def round_order(amount, asset): """ - Convert number of shares to an integer. - - By default, truncates to the integer share count that's either within - .0001 of amount or closer to zero. - - E.g. 3.9999 -> 4.0; 5.5 -> 5.0; -5.5 -> -5.0 + Converts the number of shares to the smallest tradable lot size for + the asset being ordered. + """ - return int(round_if_near_integer(amount)) + return round_nearest(amount, asset.min_trade_size) def validate_order_params(self, asset, @@ -1550,7 +1548,6 @@ class TradingAlgorithm(object): self.updated_portfolio(), self.get_datetime(), self.trading_client.current_data) - @staticmethod def __convert_order_params_for_blotter(limit_price, stop_price, style): """ diff --git a/catalyst/assets/_assets.pyx b/catalyst/assets/_assets.pyx index cdc34008..05144223 100644 --- a/catalyst/assets/_assets.pyx +++ b/catalyst/assets/_assets.pyx @@ -59,6 +59,7 @@ cdef class Asset: cdef readonly object exchange cdef readonly object exchange_full + cdef readonly object min_trade_size _kwargnames = frozenset({ 'sid', @@ -70,6 +71,7 @@ cdef class Asset: 'auto_close_date', 'exchange', 'exchange_full', + 'min_trade_size', }) def __init__(self, @@ -81,7 +83,8 @@ cdef class Asset: object end_date=None, object first_traded=None, object auto_close_date=None, - object exchange_full=None): + object exchange_full=None, + object min_trade_size=None): self.sid = sid self.sid_hash = hash(sid) @@ -94,6 +97,7 @@ cdef class Asset: self.end_date = end_date self.first_traded = first_traded self.auto_close_date = auto_close_date + self.min_trade_size = min_trade_size def __int__(self): return self.sid @@ -148,7 +152,8 @@ cdef class Asset: def __repr__(self): attrs = ('symbol', 'asset_name', 'exchange', - 'start_date', 'end_date', 'first_traded', 'auto_close_date') + 'start_date', 'end_date', 'first_traded', 'auto_close_date', + 'min_trade_size') tuples = ((attr, repr(getattr(self, attr, None))) for attr in attrs) strings = ('%s=%s' % (t[0], t[1]) for t in tuples) @@ -170,7 +175,8 @@ cdef class Asset: self.end_date, self.first_traded, self.auto_close_date, - self.exchange_full)) + self.exchange_full, + self.min_trade_size)) cpdef to_dict(self): """ @@ -186,6 +192,7 @@ cdef class Asset: 'auto_close_date': self.auto_close_date, 'exchange': self.exchange, 'exchange_full': self.exchange_full, + 'min_trade_size': self.min_trade_size } @classmethod @@ -234,7 +241,7 @@ cdef class Equity(Asset): def __repr__(self): attrs = ('symbol', 'asset_name', 'exchange', 'start_date', 'end_date', 'first_traded', 'auto_close_date', - 'exchange_full') + 'exchange_full', 'min_trade_size') tuples = ((attr, repr(getattr(self, attr, None))) for attr in attrs) strings = ('%s=%s' % (t[0], t[1]) for t in tuples) diff --git a/catalyst/assets/asset_db_schema.py b/catalyst/assets/asset_db_schema.py index 9c9c98c2..35e062f9 100644 --- a/catalyst/assets/asset_db_schema.py +++ b/catalyst/assets/asset_db_schema.py @@ -39,7 +39,8 @@ equities = sa.Table( sa.Column('first_traded', sa.Integer), sa.Column('auto_close_date', sa.Integer), sa.Column('exchange', sa.Text), - sa.Column('exchange_full', sa.Text) + sa.Column('exchange_full', sa.Text), + sa.Column('min_trade_size', sa.Float) ) equity_symbol_mappings = sa.Table( diff --git a/catalyst/assets/asset_writer.py b/catalyst/assets/asset_writer.py index 9910db72..1ea5afb2 100644 --- a/catalyst/assets/asset_writer.py +++ b/catalyst/assets/asset_writer.py @@ -73,6 +73,7 @@ _equities_defaults = { 'exchange': None, # optional, something like "New York Stock Exchange" 'exchange_full': None, + 'min_trade_size': None } # Default values for the futures DataFrame diff --git a/catalyst/finance/slippage.py b/catalyst/finance/slippage.py index 118a5b58..cfba2683 100644 --- a/catalyst/finance/slippage.py +++ b/catalyst/finance/slippage.py @@ -41,6 +41,7 @@ DEFAULT_EQUITY_VOLUME_SLIPPAGE_BAR_LIMIT = 0.025 DEFAULT_FUTURE_VOLUME_SLIPPAGE_BAR_LIMIT = 0.05 + class LiquidityExceeded(Exception): pass @@ -205,12 +206,14 @@ class VolumeShareSlippage(SlippageModel): def process_order(self, data, order): volume = data.current(order.asset, "volume") + min_trade_size = order.asset.min_trade_size + max_volume = self.volume_limit * volume # price impact accounts for the total volume of transactions # created against the current minute bar remaining_volume = max_volume - self.volume_for_bar - if remaining_volume < 1: + if remaining_volume < min_trade_size: # we can't fill any more transactions raise LiquidityExceeded() @@ -218,7 +221,7 @@ class VolumeShareSlippage(SlippageModel): # volume available in the bar or the open amount. cur_volume = int(min(remaining_volume, abs(order.open_amount))) - if cur_volume < 1: + if cur_volume < min_trade_size: return None, None # tally the current amount into our total amount ordered. diff --git a/catalyst/finance/transaction.py b/catalyst/finance/transaction.py index 3a388890..8d215dcc 100644 --- a/catalyst/finance/transaction.py +++ b/catalyst/finance/transaction.py @@ -65,14 +65,10 @@ def create_transaction(order, dt, price, amount): # floor the amount to protect against non-whole number orders # TODO: Investigate whether we can add a robust check in blotter # and/or tradesimulation, as well. - amount_magnitude = int(abs(amount)) - - if amount_magnitude < 1: - raise Exception("Transaction magnitude must be at least 1.") transaction = Transaction( asset=order.asset, - amount=int(amount), + amount=amounts, dt=dt, price=price, order_id=order.id diff --git a/catalyst/utils/math_utils.py b/catalyst/utils/math_utils.py index 16fdb99d..7981a52b 100644 --- a/catalyst/utils/math_utils.py +++ b/catalyst/utils/math_utils.py @@ -17,6 +17,8 @@ import math from numpy import isnan +def round_nearest(x, a): + return round(round(x / a) * a, -int(math.floor(math.log10(a)))) def tolerant_equals(a, b, atol=10e-7, rtol=10e-7, equal_nan=False): """Check if a and b are equal with some tolerance.