def test_volume_share_slippage(self): slippage_model = VolumeShareSlippage() open_orders = [ Order(dt=datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), amount=100, filled=0, asset=self.ASSET133) ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[0], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 1) _, txn = orders_txns[0] expected_txn = { 'price': float(3.0001875), 'dt': datetime.datetime(2006, 1, 5, 14, 31, tzinfo=pytz.utc), 'amount': int(5), 'asset': self.ASSET133, 'commission': None, 'type': DATASOURCE_TYPE.TRANSACTION, 'order_id': open_orders[0].id } self.assertIsNotNone(txn) # TODO: Make expected_txn an Transaction object and ensure there # is a __eq__ for that class. self.assertEquals(expected_txn, txn.__dict__) open_orders = [ Order(dt=datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), amount=100, filled=0, asset=self.ASSET133) ] # Set bar_data to be a minute ahead of last trade. # Volume share slippage should not execute when there is no trade. bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[1], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0)
def _create_order(self, order_status): log.info( 'creating catalyst order from Bittrex {}'.format(order_status)) if order_status['CancelInitiated']: status = ORDER_STATUS.CANCELLED elif order_status['Closed'] is not None: status = ORDER_STATUS.FILLED else: status = ORDER_STATUS.OPEN date = pd.to_datetime(order_status['Opened'], utc=True) amount = order_status['Quantity'] filled = amount - order_status['QuantityRemaining'] order = Order( dt=date, asset=self.assets[order_status['Exchange']], amount=amount, stop=None, # Not yet supported by Bittrex limit=order_status['Limit'], filled=filled, id=order_status['OrderUuid'], commission=order_status['CommissionPaid']) order.status = status executed_price = order_status['PricePerUnit'] return order, executed_price
def _create_order(self, order_status): """ Create a Catalyst order object from the Exchange order dictionary :param order_status: :return: Order """ # if order_status['is_cancelled']: # status = ORDER_STATUS.CANCELLED # elif not order_status['is_live']: # log.info('found executed order {}'.format(order_status)) # status = ORDER_STATUS.FILLED # else: status = ORDER_STATUS.OPEN amount = float(order_status['amount']) # filled = float(order_status['executed_amount']) filled = None if order_status['type'] == 'sell': amount = -amount # filled = -filled price = float(order_status['rate']) order_type = order_status['type'] stop_price = None limit_price = None # TODO: is this comprehensive enough? # if order_type.endswith('limit'): # limit_price = price # elif order_type.endswith('stop'): # stop_price = price # executed_price = float(order_status['avg_execution_price']) executed_price = price # TODO: bitfinex does not specify comission. I could calculate it but not sure if it's worth it. commission = None # date = pd.Timestamp.utcfromtimestamp(float(order_status['timestamp'])) # date = pytz.utc.localize(date) date = None order = Order( dt=date, asset=self.assets[order_status['symbol']], # No such field in Poloniex amount=amount, stop=stop_price, limit=limit_price, filled=filled, id=str(order_status['orderNumber']), commission=commission ) order.status = status return order, executed_price
def _create_order(self, order_status): """ Create a Catalyst order object from a Bitfinex order dictionary :param order_status: :return: Order """ if order_status['is_cancelled']: status = ORDER_STATUS.CANCELLED elif not order_status['is_live']: log.info('found executed order {}'.format(order_status)) status = ORDER_STATUS.FILLED else: status = ORDER_STATUS.OPEN amount = float(order_status['original_amount']) filled = float(order_status['executed_amount']) if order_status['side'] == 'sell': amount = -amount filled = -filled price = float(order_status['price']) order_type = order_status['type'] stop_price = None limit_price = None # TODO: is this comprehensive enough? if order_type.endswith('limit'): limit_price = price elif order_type.endswith('stop'): stop_price = price executed_price = float(order_status['avg_execution_price']) # TODO: bitfinex does not specify comission. # I could calculate it but not sure if it's worth it. commission = None date = pd.Timestamp.utcfromtimestamp(float(order_status['timestamp'])) date = pytz.utc.localize(date) order = Order(dt=date, asset=self.assets[order_status['symbol']], amount=amount, stop=stop_price, limit=limit_price, filled=filled, id=str(order_status['id']), commission=commission) order.status = status return order, executed_price
def test_calculate_impact_without_history(self): model = VolatilityVolumeShare(volume_limit=1) 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'), late_start_asset), # Only a week's worth of history data. (pd.Timestamp('2006-02-17 11:35AM', tz='UTC'), late_start_asset), ] for minute, asset in cases: data = self.create_bardata(simulation_dt_func=lambda: minute) order = Order(dt=data.current_dt, asset=asset, amount=10) price, amount = model.process_order(data, order) avg_price = (data.current(asset, 'high') + data.current(asset, 'low')) / 2 expected_price = \ avg_price * (1 + model.NO_DATA_VOLATILITY_SLIPPAGE_IMPACT) self.assertAlmostEqual(price, expected_price, delta=0.001) self.assertEqual(amount, 10)
def _handle_request_timeout(self, dt_before, asset, amount, is_buy, style, adj_amount): """ Check if an order was received during the timeout, if it appeared on the orders dict return it to the user. If an order_id was traced alone, an order is created manually and returned to the user. Otherwise, send none to raise an error and retry the call. :param dt_before: pd.Timestamp :param asset: Asset :param amount: float :param is_buy: Bool :param style: :param adj_amount: int :return: missing_order: Order/ None """ missing_order_id, missing_order = self._fetch_missing_order( dt_before=dt_before, symbol=asset.asset_name) if missing_order_id: final_amount = adj_amount if amount > 0 else -adj_amount missing_order = Order(dt=dt_before, asset=asset, amount=final_amount, stop=style.get_stop_price(is_buy), limit=style.get_limit_price(is_buy), id=missing_order_id) return missing_order
def test_process_order_timeout(self): """ in case of a requestTimeout make sure that the process_order method returns an exception so the retry method can request the trades again. :return: """ asset = [ pair for pair in self.exchange.assets if pair.symbol == 'eth_usdt' ][0] amount = 2 price = 0.0025 order = Order(dt=pd.to_datetime('2018-05-01 19:54', utc=True), asset=asset, amount=amount, stop=None, limit=price, id='111') self.exchange.api = MagicMock(spec=[ u'create_order', u'fetch_my_trades', u'has', u'fetch_open_orders', u'orders', u'fetch_closed_orders' ]) self.exchange.api.has = { 'fetchClosedOrders': 'emulated', 'fetchOrders': False, 'fetchMyTrades': True, } with patch('catalyst.exchange.ccxt.ccxt_exchange.CCXT.get_trades') as \ mock_trades: mock_trades.side_effect = RequestTimeout try: observed_transactions = self.exchange.process_order(order) except ExchangeRequestError as e: pass
def generate_order_and_txns(self, sid, order_amount, fill_amounts): asset1 = self.asset_finder.retrieve_asset(sid) # one order order = Order(dt=None, asset=asset1, amount=order_amount) # three fills txn1 = Transaction(asset=asset1, amount=fill_amounts[0], dt=None, price=100, order_id=order.id) txn2 = Transaction(asset=asset1, amount=fill_amounts[1], dt=None, price=101, order_id=order.id) txn3 = Transaction(asset=asset1, amount=fill_amounts[2], dt=None, price=102, order_id=order.id) return order, [txn1, txn2, txn3]
def create_order(self, asset, amount, is_buy, style): """ Creating order on the exchange. :param asset: :param amount: :param is_buy: :param style: :return: """ exchange_symbol = self.get_symbol(asset) if isinstance(style, ExchangeLimitOrder) \ or isinstance(style, ExchangeStopLimitOrder): price = style.get_limit_price(is_buy) order_type = 'limit' elif isinstance(style, ExchangeStopOrder): price = style.get_stop_price(is_buy) order_type = 'stop' else: raise InvalidOrderStyle(exchange=self.name, style=style.__class__.__name__) req = dict( symbol=exchange_symbol, amount=str(float(abs(amount))), price="{:.20f}".format(float(price)), side='buy' if is_buy else 'sell', type='exchange ' + order_type, # TODO: support margin trades exchange=self.name, is_hidden=False, is_postonly=False, use_all_available=0, ocoorder=False, buy_price_oco=0, sell_price_oco=0) date = pd.Timestamp.utcnow() try: self.ask_request() response = self._request('order/new', req) order_status = response.json() except Exception as e: raise ExchangeRequestError(error=e) if 'message' in order_status: raise ExchangeRequestError( error='unable to create Bitfinex order {}'.format( order_status['message'])) order_id = str(order_status['id']) order = Order(dt=date, asset=asset, amount=amount, stop=style.get_stop_price(is_buy), limit=style.get_limit_price(is_buy), id=order_id) return order
def order(self, asset, amount, style, order_id=None): """Place an order. Parameters ---------- asset : catalyst.assets.Asset The asset that this order is for. amount : int The amount of shares to order. If ``amount`` is positive, this is the number of shares to buy or cover. If ``amount`` is negative, this is the number of shares to sell or short. style : catalyst.finance.execution.ExecutionStyle The execution style for the order. order_id : str, optional The unique identifier for this order. Returns ------- order_id : str or None The unique identifier for this order, or None if no order was placed. Notes ----- amount > 0 :: Buy/Cover amount < 0 :: Sell/Short Market order: order(asset, amount) Limit order: order(asset, amount, style=LimitOrder(limit_price)) Stop order: order(asset, amount, style=StopOrder(stop_price)) StopLimit order: order(asset, amount, style=StopLimitOrder(limit_price, stop_price)) """ # something could be done with amount to further divide # between buy by share count OR buy shares up to a dollar amount # numeric == share count AND "$dollar.cents" == cost amount if amount == 0: # Don't bother placing orders for 0 shares. return None elif amount > self.max_shares: # Arbitrary limit of 100 billion (US) shares will never be # exceeded except by a buggy algorithm. raise OverflowError("Can't order more than %d shares" % self.max_shares) is_buy = (amount > 0) order = Order( dt=self.current_dt, asset=asset, amount=amount, stop=style.get_stop_price(is_buy), limit=style.get_limit_price(is_buy), id=order_id ) self.open_orders[order.asset].append(order) self.orders[order.id] = order self.new_orders.append(order) return order.id
def create_order(self, asset, amount, is_buy, style): log.info('creating {} order'.format('buy' if is_buy else 'sell')) exchange_symbol = self.get_symbol(asset) if isinstance(style, LimitOrder) or isinstance(style, StopLimitOrder): if isinstance(style, StopLimitOrder): log.warn('{} will ignore the stop price'.format(self.name)) price = style.get_limit_price(is_buy) try: if is_buy: order_status = self.api.buylimit(exchange_symbol, amount, price) else: order_status = self.api.selllimit(exchange_symbol, abs(amount), price) except Exception as e: raise ExchangeRequestError(error=e) if 'uuid' in order_status: order_id = order_status['uuid'] order = Order(dt=pd.Timestamp.utcnow(), asset=asset, amount=amount, stop=style.get_stop_price(is_buy), limit=style.get_limit_price(is_buy), id=order_id) return order else: raise CreateOrderError(exchange=self.name, error=order_status) else: raise InvalidOrderStyle(exchange=self.name, style=style.__class__.__name__)
def test_orders_stop(self, name, order_data, event_data, expected): data = order_data data['asset'] = self.ASSET133 order = Order(**data) if expected['transaction']: expected['transaction']['asset'] = self.ASSET133 event_data['asset'] = self.ASSET133 assets = ((133, pd.DataFrame( { 'open': [event_data['open']], 'high': [event_data['high']], 'low': [event_data['low']], 'close': [event_data['close']], 'volume': [event_data['volume']], }, index=[pd.Timestamp('2006-01-05 14:31', tz='UTC')], )), ) days = pd.date_range(start=normalize_date(self.minutes[0]), end=normalize_date(self.minutes[-1])) with tmp_bcolz_equity_minute_bar_reader(self.trading_calendar, days, assets) as reader: data_portal = DataPortal( self.env.asset_finder, self.trading_calendar, first_trading_day=reader.first_trading_day, equity_minute_reader=reader, ) slippage_model = VolumeShareSlippage() try: dt = pd.Timestamp('2006-01-05 14:31', tz='UTC') bar_data = BarData( data_portal, lambda: dt, self.sim_params.data_frequency, self.trading_calendar, NoRestrictions(), ) _, txn = next( slippage_model.simulate( bar_data, self.ASSET133, [order], )) except StopIteration: txn = None if expected['transaction'] is None: self.assertIsNone(txn) else: self.assertIsNotNone(txn) for key, value in expected['transaction'].items(): self.assertEquals(value, txn[key])
def create_order(self, asset, amount, is_buy, style): symbol = self.get_symbol(asset) if isinstance(style, ExchangeLimitOrder): price = style.get_limit_price(is_buy) order_type = 'limit' elif isinstance(style, MarketOrder): price = None order_type = 'market' else: raise InvalidOrderStyle(exchange=self.name, style=style.__class__.__name__) side = 'buy' if amount > 0 else 'sell' if hasattr(self.api, 'amount_to_lots'): adj_amount = self.api.amount_to_lots( symbol=symbol, amount=abs(amount), ) if adj_amount != abs(amount): log.info( 'adjusted order amount {} to {} based on lot size'.format( abs(amount), adj_amount, )) else: adj_amount = abs(amount) try: result = self.api.create_order(symbol=symbol, type=order_type, side=side, amount=adj_amount, price=price) except ExchangeNotAvailable as e: log.debug('unable to create order: {}'.format(e)) raise ExchangeRequestError(error=e) except InvalidOrder as e: log.warn('the exchange rejected the order: {}'.format(e)) raise CreateOrderError(exchange=self.name, error=e) if 'info' not in result: raise ValueError('cannot use order without info attribute') final_amount = adj_amount if side == 'buy' else -adj_amount order_id = result['id'] order = Order(dt=pd.Timestamp.utcnow(), asset=asset, amount=final_amount, stop=style.get_stop_price(is_buy), limit=style.get_limit_price(is_buy), id=order_id) return order
def create_orders_dict(self, asset, last_order): """ create an orders dict which mocks the .orders object :param asset: TradingPair :param last_order: bool, adds another order to the dict. mocks the functionality of the fetchOrder methods :return: dict(Order) """ orders = dict() orders['208612980769'] = Order( dt=pd.to_datetime('2018-05-01 17:34', utc=True), asset=asset, amount=2, stop=None, limit=0.0025, id='208612980769' ) orders['656797594'] = Order( dt=pd.to_datetime('2018-05-01 18:34', utc=True), asset=asset, amount=1, stop=None, limit=0.0027, id='656797594' ) orders['656797494'] = Order( dt=pd.to_datetime('2018-05-01 18:54', utc=True), asset=asset, amount=7, stop=None, limit=0.00246, id='656797494' ) if last_order: orders['111'] = Order( dt=pd.to_datetime('2018-05-01 19:54', utc=True), asset=asset, amount=2, stop=None, limit=0.00254, id='111' ) return orders
def test_create_order_timeout_open(self): """ create_order method tests the handling of a RequestTimeout exception and locating the order, if was created, using the fetchOpenOrders method :return: """ asset = [pair for pair in self.exchange.assets if pair.symbol == 'eth_usdt'][0] amount = 2 is_buy = True self.last_order = False price = 0.00254 self.exchange.api = MagicMock( spec=[u'create_order', u'fetch_open_orders', u'fetch_orders', u'orders', u'has', u'amount_to_precision' ] ) self.exchange.api.create_order.side_effect = RequestTimeout orders_dict = self.create_orders_dict(asset, self.last_order) self.exchange.api.orders = orders_dict self.exchange.api.has = {'fetchOpenOrders': True, 'fetchOrders': 'emulated', 'fetchClosedOrders': True } self.exchange.api.fetch_open_orders.side_effect = self.mod_last_order mock_style = create_autospec(ExchangeLimitOrder, return_value=price) mock_style.get_limit_price.return_value = price style = mock_style self.exchange.api.amount_to_precision = Mock( return_value=float(amount)) with patch('catalyst.exchange.ccxt.ccxt_exchange.CCXT.get_symbol') as \ mock_symbol: mock_symbol.return_value = 'ETH/USDT' observed_fetchOpen_order = self.exchange.create_order( asset, amount, is_buy, style) expected_fetchOpen_order = Order( dt=pd.to_datetime('2018-05-01 19:54', utc=True), asset=asset, amount=amount, stop=None, limit=price, id='111' ) assert self.compare_orders(observed_fetchOpen_order, expected_fetchOpen_order) is True
def test_calculate_impact_buy(self): answer_key = [ # We ordered 10 contracts, but are capped at 100 * 0.05 = 5 (91485.500085168125, 5), (91486.500085169057, 5), (None, None), ] order = Order( dt=pd.Timestamp.now(tz='utc').round('min'), asset=self.ASSET, amount=10, ) self._calculate_impact(order, answer_key)
def test_low_transaction_volume(self): # With a volume limit of 0.001, and a bar volume of 100, we should # compute a transaction volume of 100 * 0.001 = 0.1, which gets rounded # down to zero. In this case we expect no amount to be transacted. model = VolatilityVolumeShare(volume_limit=0.001) minute = pd.Timestamp('2006-03-01 11:35AM', tz='UTC') data = self.create_bardata(simulation_dt_func=lambda: minute) order = Order(dt=data.current_dt, asset=self.ASSET, amount=10) price, amount = model.process_order(data, order) self.assertIsNone(price) self.assertIsNone(amount)
def test_calculate_impact_sell(self): answer_key = [ # We ordered -10 contracts, but are capped at -(100 * 0.05) = -5 (91485.499914831875, -5), (91486.499914830943, -5), (None, None), ] order = Order( dt=pd.Timestamp.now(tz='utc').round('min'), asset=self.ASSET, amount=-10, ) self._calculate_impact(order, answer_key)
def create_order(self, asset, amount, is_buy, style): """ Creating order on the exchange. :param asset: :param amount: :param is_buy: :param style: :return: """ exchange_symbol = self.get_symbol(asset) if isinstance(style, ExchangeLimitOrder) or isinstance(style, ExchangeStopLimitOrder): if isinstance(style, ExchangeStopLimitOrder): log.warn('{} will ignore the stop price'.format(self.name)) price = style.get_limit_price(is_buy) try: if (is_buy): response = self.api.buy(exchange_symbol, amount, price) else: response = self.api.sell(exchange_symbol, -amount, price) except Exception as e: raise ExchangeRequestError(error=e) date = pd.Timestamp.utcnow() if ('orderNumber' in response): order_id = str(response['orderNumber']) order = Order( dt=date, asset=asset, amount=amount, stop=style.get_stop_price(is_buy), limit=style.get_limit_price(is_buy), id=order_id ) return order else: log.warn( '{} order failed: {}'.format('buy' if is_buy else 'sell', response['error'])) return None else: raise InvalidOrderStyle(exchange=self.name, style=style.__class__.__name__)
def test_impacted_price_worse_than_limit(self): model = VolatilityVolumeShare(volume_limit=0.05) # Use all the same numbers from the 'calculate_impact' tests. Since the # impacted price is 59805.5, which is worse than the limit price of # 59800, the model should return None. minute = pd.Timestamp('2006-03-01 11:35AM', tz='UTC') data = self.create_bardata(simulation_dt_func=lambda: minute) order = Order( dt=data.current_dt, asset=self.ASSET, amount=10, limit=59800, ) price, amount = model.process_order(data, order) self.assertIsNone(price) self.assertIsNone(amount)
def create_order(self, asset, amount, is_buy, style): log.info('creating {} order'.format('buy' if is_buy else 'sell')) exchange_symbol = self.get_symbol(asset) if isinstance(style, LimitOrder) or isinstance(style, StopLimitOrder): if isinstance(style, StopLimitOrder): log.warn('{} will ignore the stop price'.format(self.name)) price = style.get_limit_price(is_buy) try: self.ask_request() if is_buy: order_status = self.api.buylimit(exchange_symbol, amount, price) else: order_status = self.api.selllimit(exchange_symbol, abs(amount), price) except Exception as e: raise ExchangeRequestError(error=e) if 'uuid' in order_status: order_id = order_status['uuid'] order = Order(dt=pd.Timestamp.utcnow(), asset=asset, amount=amount, stop=style.get_stop_price(is_buy), limit=style.get_limit_price(is_buy), id=order_id) return order else: if order_status == 'INSUFFICIENT_FUNDS': log.warn('not enough funds to create order') return None elif order_status == 'DUST_TRADE_DISALLOWED_MIN_VALUE_50K_SAT': log.warn('Your order is too small, order at least 50K' ' Satoshi') return None else: raise CreateOrderError(exchange=self.name, error=order_status) else: raise InvalidOrderStyle(exchange=self.name, style=style.__class__.__name__)
def test_volume_share_slippage_with_future(self): slippage_model = VolumeShareSlippage(volume_limit=1, price_impact=0.3) open_orders = [ Order( dt=datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), amount=10, filled=0, asset=self.ASSET1000, ), ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[0], ) orders_txns = list( slippage_model.simulate(bar_data, self.ASSET1000, open_orders)) self.assertEquals(len(orders_txns), 1) _, txn = orders_txns[0] # We expect to fill the order for all 10 contracts. The volume for the # futures contract in this bar is 100, so our volume share is: # 10.0 / 100 = 0.1 # The current price is 5.0 and the price impact is 0.3, so the expected # impacted price is: # 5.0 + (5.0 * (0.1 ** 2) * 0.3) = 5.015 expected_txn = { 'price': 5.015, 'dt': datetime.datetime(2006, 1, 5, 14, 31, tzinfo=pytz.utc), 'amount': 10, 'asset': self.ASSET1000, 'commission': None, 'type': DATASOURCE_TYPE.TRANSACTION, 'order_id': open_orders[0].id, } self.assertIsNotNone(txn) self.assertEquals(expected_txn, txn.__dict__)
def test_prune_orders(self): blotter = Blotter(self.sim_params.data_frequency) blotter.order(self.asset_24, 100, MarketOrder()) open_order = blotter.open_orders[self.asset_24][0] blotter.prune_orders([]) self.assertEqual(1, len(blotter.open_orders[self.asset_24])) blotter.prune_orders([open_order]) self.assertEqual(0, len(blotter.open_orders[self.asset_24])) # prune an order that isn't in our our open orders list, make sure # nothing blows up other_order = Order( dt=blotter.current_dt, asset=self.asset_25, amount=1 ) blotter.prune_orders([other_order])
def _calculate_impact(self, test_order, answer_key): model = VolatilityVolumeShare(volume_limit=0.05) first_minute = pd.Timestamp('2006-03-31 11:35AM', tz='UTC') next_3_minutes = self.trading_calendar.minutes_window(first_minute, 3) remaining_shares = test_order.open_amount for i, minute in enumerate(next_3_minutes): data = self.create_bardata(simulation_dt_func=lambda: minute) new_order = Order( dt=data.current_dt, asset=self.ASSET, amount=remaining_shares, ) price, amount = model.process_order(data, new_order) self.assertEqual(price, answer_key[i][0]) self.assertEqual(amount, answer_key[i][1]) amount = amount or 0 if remaining_shares < 0: remaining_shares = min(0, remaining_shares - amount) else: remaining_shares = max(0, remaining_shares - amount)
def make_order(self, amount): return Order( self.minute, self.asset, amount, )
def _create_order(self, order_status): """ Create a Catalyst order object from a CCXT order dictionary Parameters ---------- order_status: dict[str, Object] The order dict from the CCXT api. Returns ------- Order The Catalyst order object """ order_id = order_status['id'] symbol = self.get_symbol(order_status['symbol'], source='ccxt') asset = self.get_asset(symbol) s = order_status['status'] amount = order_status['amount'] filled = order_status['filled'] if s == 'canceled' or (s == 'closed' and filled == 0): status = ORDER_STATUS.CANCELLED elif s == 'closed' and filled > 0: if filled < amount: log.warn( 'order {id} is executed but only partially filled:' ' {filled} {symbol} out of {amount}'.format( id=order_status['status'], filled=order_status['filled'], symbol=asset.symbol, amount=order_status['amount'], ) ) else: log.info( 'order {id} executed in full: {filled} {symbol}'.format( id=order_id, filled=filled, symbol=asset.symbol, ) ) status = ORDER_STATUS.FILLED elif s == 'open': status = ORDER_STATUS.OPEN elif filled > 0: log.info( 'order {id} partially filled: {filled} {symbol} out of ' '{amount}, waiting for complete execution'.format( id=order_id, filled=filled, symbol=asset.symbol, amount=amount, ) ) status = ORDER_STATUS.OPEN else: log.warn( 'invalid state {} for order {}'.format( s, order_id ) ) status = ORDER_STATUS.OPEN if order_status['side'] == 'sell': amount = -amount filled = -filled price = order_status['price'] order_type = order_status['type'] limit_price = price if order_type == 'limit' else None executed_price = order_status['cost'] / order_status['amount'] commission = order_status['fee'] date = from_ms_timestamp(order_status['timestamp']) order = Order( dt=date, asset=asset, amount=amount, stop=None, limit=limit_price, filled=filled, id=order_id, commission=commission ) order.status = status return order, executed_price
def test_create_order_timeout_trade(self): """ create_order method tests the handling of a RequestTimeout exception and locating the order, if was created, using the fetchTrades method. checks as well, the case that the order was not created at all, and makes sure an exception is raised in order to retry the creation of the order. :return: """ asset = [ pair for pair in self.exchange.assets if pair.symbol == 'eth_usdt' ][0] amount = 2 is_buy = True self.last_order = False self.last_trade = False price = 0.00254 stop_price = 0.00354 self.exchange.api = MagicMock(spec=[ u'create_order', u'fetch_my_trades', u'has', u'fetch_open_orders', u'orders', u'fetch_closed_orders' ]) self.exchange.api.create_order.side_effect = RequestTimeout orders_dict = self.create_orders_dict(asset, self.last_order) self.exchange.api.orders = orders_dict self.exchange.api.has = { 'fetchClosedOrders': 'emulated', 'fetchOrders': False, 'fetchMyTrades': True, } self.exchange.api.fetch_my_trades.side_effect = self.create_trades_dict mock_style = create_autospec(ExchangeLimitOrder, return_value=price) mock_style.get_limit_price.return_value = price mock_style.get_stop_price.return_value = stop_price style = mock_style # check the case there are no new trades and an exception is raised with patch('catalyst.exchange.ccxt.ccxt_exchange.CCXT.get_symbol') as \ mock_symbol: mock_symbol.return_value = 'ETH/USDT' try: observed_fetchTrade_None = self.exchange.create_order( asset, amount, is_buy, style) except ExchangeRequestError as e: pass # check the case there are trades which form a neew order self.last_trade = True with patch('catalyst.exchange.ccxt.ccxt_exchange.CCXT.get_symbol') as \ mock_symbol: mock_symbol.return_value = 'ETH/USDT' observed_fetchTrade_order = self.exchange.create_order( asset, amount, is_buy, style) expected_fetchTrade_order = Order(dt=pd.Timestamp.utcnow(), asset=asset, amount=amount, stop=stop_price, limit=price, id='111') assert self.compare_orders(observed_fetchTrade_order, expected_fetchTrade_order) is True # check the case there are no new trades or orders and an exception is # raised self.last_trade = False self.exchange.api.has['fetchOpenOrders'] = True with patch('catalyst.exchange.ccxt.ccxt_exchange.CCXT.get_symbol') as \ mock_symbol: mock_symbol.return_value = 'ETH/USDT' try: observed_fetchTradeOrder_None = self.exchange.create_order( asset, amount, is_buy, style) except ExchangeRequestError as e: pass
def create_order(self, asset, amount, is_buy, style): symbol = self.get_symbol(asset) if isinstance(style, ExchangeLimitOrder): price = style.get_limit_price(is_buy) order_type = 'limit' elif isinstance(style, MarketOrder): price = None order_type = 'market' else: raise InvalidOrderStyle(exchange=self.name, style=style.__class__.__name__) side = 'buy' if amount > 0 else 'sell' if hasattr(self.api, 'amount_to_lots'): # TODO: is this right? if self.api.markets is None: self.api.load_markets() # https://github.com/ccxt/ccxt/issues/1483 adj_amount = round(abs(amount), asset.decimals) market = self.api.markets[symbol] if 'lots' in market and market['lots'] > amount: raise CreateOrderError( exchange=self.name, e='order amount lower than the smallest lot: {}'.format( amount)) else: adj_amount = round(abs(amount), asset.decimals) before_order_dt = pd.Timestamp.utcnow() try: result = self.api.create_order(symbol=symbol, type=order_type, side=side, amount=adj_amount, price=price) except InvalidOrder as e: log.warn('the exchange rejected the order: {}'.format(e)) raise CreateOrderError(exchange=self.name, error=e) except RequestTimeout as e: log.info('received a RequestTimeout exception while creating ' 'an order on {} / {}\n Checking if an order was filled ' 'during the timeout'.format(self.name, symbol)) missing_order = self._handle_request_timeout( before_order_dt, asset, amount, is_buy, style, adj_amount) if missing_order is None: # no order was found log.warn( 'no order was identified during timeout exception.' 'Please double check for inconsistency with the exchange. ' 'We encourage you to report any issue on GitHub: ' 'https://github.com/enigmampc/catalyst/issues') raise ExchangeRequestError(error=e) else: return missing_order except (ExchangeError, NetworkError) as e: log.warn('unable to create order {} / {}: {}'.format( self.name, symbol, e)) raise ExchangeRequestError(error=e) exchange_amount = None if 'amount' in result and result['amount'] != adj_amount: exchange_amount = result['amount'] elif 'info' in result: if 'origQty' in result['info']: exchange_amount = float(result['info']['origQty']) if exchange_amount: log.info('order amount adjusted by {} from {} to {}'.format( self.name, adj_amount, exchange_amount)) adj_amount = exchange_amount if 'info' not in result: raise ValueError('cannot use order without info attribute') final_amount = adj_amount if side == 'buy' else -adj_amount order_id = result['id'] order = Order(dt=pd.Timestamp.utcnow(), asset=asset, amount=final_amount, stop=style.get_stop_price(is_buy), limit=style.get_limit_price(is_buy), id=order_id) return order
def _create_order(self, order_status): """ Create a Catalyst order object from a CCXT order dictionary Parameters ---------- order_status: dict[str, Object] The order dict from the CCXT api. Returns ------- Order The Catalyst order object """ order_id = order_status['id'] symbol = self.get_symbol(order_status['symbol'], source='ccxt') asset = self.get_asset(symbol) s = order_status['status'] amount = order_status['amount'] filled = order_status['filled'] if s == 'canceled' or (s == 'closed' and filled == 0): status = ORDER_STATUS.CANCELLED elif s == 'closed' and filled > 0: if filled < amount: log.warn( 'order {id} is executed but only partially filled:' ' {filled} {symbol} out of {amount}'.format( id=order_status['status'], filled=order_status['filled'], symbol=asset.symbol, amount=order_status['amount'], ) ) else: log.info( 'order {id} executed in full: {filled} {symbol}'.format( id=order_id, filled=filled, symbol=asset.symbol, ) ) status = ORDER_STATUS.FILLED elif s == 'open': status = ORDER_STATUS.OPEN elif filled > 0: log.info( 'order {id} partially filled: {filled} {symbol} out of ' '{amount}, waiting for complete execution'.format( id=order_id, filled=filled, symbol=asset.symbol, amount=amount, ) ) status = ORDER_STATUS.OPEN else: log.warn( 'invalid state {} for order {}'.format( s, order_id ) ) status = ORDER_STATUS.OPEN if order_status['side'] == 'sell': amount = -amount filled = -filled price = order_status['price'] order_type = order_status['type'] limit_price = price if order_type == 'limit' else None executed_price = order_status['cost'] / order_status['amount'] commission = order_status['fee'] date = from_ms_timestamp(order_status['timestamp']) order = Order( dt=date, asset=asset, amount=amount, stop=None, limit=limit_price, filled=filled, id=order_id, commission=commission ) order.status = status return order, executed_price
def test_orders_limit(self): slippage_model = VolumeShareSlippage() slippage_model.data_portal = self.data_portal # long, does not trade open_orders = [ Order( **{ 'dt': datetime.datetime( 2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': 100, 'filled': 0, 'asset': self.ASSET133, 'limit': 3.5 }) ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[3], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) # long, does not trade - impacted price worse than limit price open_orders = [ Order( **{ 'dt': datetime.datetime( 2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': 100, 'filled': 0, 'asset': self.ASSET133, 'limit': 3.5 }) ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[3], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) # long, does trade open_orders = [ Order( **{ 'dt': datetime.datetime( 2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': 100, 'filled': 0, 'asset': self.ASSET133, 'limit': 3.6 }) ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[3], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 1) txn = orders_txns[0][1] expected_txn = { 'price': float(3.50021875), 'dt': datetime.datetime(2006, 1, 5, 14, 34, tzinfo=pytz.utc), # we ordered 100 shares, but default volume slippage only allows # for 2.5% of the volume. 2.5% * 2000 = 50 shares 'amount': int(50), 'asset': self.ASSET133, 'order_id': open_orders[0].id } self.assertIsNotNone(txn) for key, value in expected_txn.items(): self.assertEquals(value, txn[key]) # short, does not trade open_orders = [ Order( **{ 'dt': datetime.datetime( 2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': -100, 'filled': 0, 'asset': self.ASSET133, 'limit': 3.5 }) ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[0], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) # short, does not trade - impacted price worse than limit price open_orders = [ Order( **{ 'dt': datetime.datetime( 2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': -100, 'filled': 0, 'asset': self.ASSET133, 'limit': 3.5 }) ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[0], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) # short, does trade open_orders = [ Order( **{ 'dt': datetime.datetime( 2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': -100, 'filled': 0, 'asset': self.ASSET133, 'limit': 3.4 }) ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[1], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 1) _, txn = orders_txns[0] expected_txn = { 'price': float(3.49978125), 'dt': datetime.datetime(2006, 1, 5, 14, 32, tzinfo=pytz.utc), 'amount': int(-50), 'asset': self.ASSET133, } self.assertIsNotNone(txn) for key, value in expected_txn.items(): self.assertEquals(value, txn[key])
def test_orders_stop_limit(self): slippage_model = VolumeShareSlippage() slippage_model.data_portal = self.data_portal # long, does not trade open_orders = [ Order( **{ 'dt': datetime.datetime( 2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': 100, 'filled': 0, 'asset': self.ASSET133, 'stop': 4.0, 'limit': 3.0 }) ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[2], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[3], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) # long, does not trade - impacted price worse than limit price open_orders = [ Order( **{ 'dt': datetime.datetime( 2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': 100, 'filled': 0, 'asset': self.ASSET133, 'stop': 4.0, 'limit': 3.5 }) ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[2], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[3], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) # long, does trade open_orders = [ Order( **{ 'dt': datetime.datetime( 2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': 100, 'filled': 0, 'asset': self.ASSET133, 'stop': 4.0, 'limit': 3.6 }) ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[2], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[3], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 1) _, txn = orders_txns[0] expected_txn = { 'price': float(3.50021875), 'dt': datetime.datetime(2006, 1, 5, 14, 34, tzinfo=pytz.utc), 'amount': int(50), 'asset': self.ASSET133 } for key, value in expected_txn.items(): self.assertEquals(value, txn[key]) # short, does not trade open_orders = [ Order( **{ 'dt': datetime.datetime( 2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': -100, 'filled': 0, 'asset': self.ASSET133, 'stop': 3.0, 'limit': 4.0 }) ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[0], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[1], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) # short, does not trade - impacted price worse than limit price open_orders = [ Order( **{ 'dt': datetime.datetime( 2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': -100, 'filled': 0, 'asset': self.ASSET133, 'stop': 3.0, 'limit': 3.5 }) ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[0], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[1], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) # short, does trade open_orders = [ Order( **{ 'dt': datetime.datetime( 2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': -100, 'filled': 0, 'asset': self.ASSET133, 'stop': 3.0, 'limit': 3.4 }) ] bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[0], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 0) bar_data = self.create_bardata( simulation_dt_func=lambda: self.minutes[1], ) orders_txns = list( slippage_model.simulate( bar_data, self.ASSET133, open_orders, )) self.assertEquals(len(orders_txns), 1) _, txn = orders_txns[0] expected_txn = { 'price': float(3.49978125), 'dt': datetime.datetime(2006, 1, 5, 14, 32, tzinfo=pytz.utc), 'amount': int(-50), 'asset': self.ASSET133, } for key, value in expected_txn.items(): self.assertEquals(value, txn[key])