From e5a137f205fc55760832abd5db3fb906b27ffb44 Mon Sep 17 00:00:00 2001 From: Andrew Campbell Date: Fri, 21 Jul 2017 18:05:32 -0500 Subject: [PATCH 1/2] 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. From c3897cfa5a1f598a6575337102c48dadb82c6f74 Mon Sep 17 00:00:00 2001 From: Victor Grau Serrat Date: Thu, 14 Sep 2017 15:22:51 -0600 Subject: [PATCH 2/2] ENH: wrapping up asset min_trade_size for fractional coinsup to 1/100000000th of a coin --- catalyst/assets/asset_writer.py | 4 +++- catalyst/data/bundles/base_pricing.py | 1 + catalyst/data/bundles/poloniex.py | 2 ++ catalyst/finance/slippage.py | 2 +- catalyst/finance/transaction.py | 2 +- 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/catalyst/assets/asset_writer.py b/catalyst/assets/asset_writer.py index 1ea5afb2..ec30ffab 100644 --- a/catalyst/assets/asset_writer.py +++ b/catalyst/assets/asset_writer.py @@ -73,7 +73,7 @@ _equities_defaults = { 'exchange': None, # optional, something like "New York Stock Exchange" 'exchange_full': None, - 'min_trade_size': None + 'min_trade_size': 1 } # Default values for the futures DataFrame @@ -391,6 +391,8 @@ class AssetDBWriter(object): The date on which to close any positions in this asset. exchange : str The exchange where this asset is traded. + min_trade_size: float, optional + The minimum denomination this asset can be traded. The index of this dataframe should contain the sids. futures : pd.DataFrame, optional diff --git a/catalyst/data/bundles/base_pricing.py b/catalyst/data/bundles/base_pricing.py index a0abd51a..c5281fdd 100644 --- a/catalyst/data/bundles/base_pricing.py +++ b/catalyst/data/bundles/base_pricing.py @@ -24,6 +24,7 @@ class BasePricingBundle(BaseBundle): ('start_date', 'datetime64[ns]'), ('end_date', 'datetime64[ns]'), ('ac_date', 'datetime64[ns]'), + ('min_trade_size', 'float'), ] @lazyval diff --git a/catalyst/data/bundles/poloniex.py b/catalyst/data/bundles/poloniex.py index eb4fc735..8b5628d5 100644 --- a/catalyst/data/bundles/poloniex.py +++ b/catalyst/data/bundles/poloniex.py @@ -75,12 +75,14 @@ class PoloniexBundle(BaseCryptoPricingBundle): start_date = sym_data.index[0] end_date = sym_data.index[-1] ac_date = end_date + pd.Timedelta(days=1) + min_trade_size = 0.00000001 return ( sym_md.symbol, start_date, end_date, ac_date, + min_trade_size, ) def fetch_raw_symbol_frame(self, diff --git a/catalyst/finance/slippage.py b/catalyst/finance/slippage.py index cfba2683..36de4ec7 100644 --- a/catalyst/finance/slippage.py +++ b/catalyst/finance/slippage.py @@ -219,7 +219,7 @@ class VolumeShareSlippage(SlippageModel): # the current order amount will be the min of the # volume available in the bar or the open amount. - cur_volume = int(min(remaining_volume, abs(order.open_amount))) + cur_volume = min(remaining_volume, abs(order.open_amount)) if cur_volume < min_trade_size: return None, None diff --git a/catalyst/finance/transaction.py b/catalyst/finance/transaction.py index 8d215dcc..f9d72c38 100644 --- a/catalyst/finance/transaction.py +++ b/catalyst/finance/transaction.py @@ -68,7 +68,7 @@ def create_transaction(order, dt, price, amount): transaction = Transaction( asset=order.asset, - amount=amounts, + amount=amount, dt=dt, price=price, order_id=order.id