def remove(self, netting_order: MarketOrder or LimitOrder) -> float: """ Remove position from inventory and return position PnL. :param netting_order: order object used to net position :return: (bool) TRUE if position removed successfully """ pnl = 0. if self.position_count < 1: LOGGER.info('Error. No {} positions to remove.'.format(self.side)) return pnl order = self.positions.popleft() # Calculate PnL if self.side == 'long': pnl = (netting_order.price / order.average_execution_price) - 1. elif self.side == 'short': pnl = (order.average_execution_price / netting_order.price) - 1. # Add Profit and Loss to realized gains/losses self.realized_pnl += pnl # Update positions attributes self.total_exposure -= order.average_execution_price self.average_price = self.total_exposure / self.position_count if \ self.position_count > 0 else 0. self.full_inventory = self.position_count >= self.max_position_count LOGGER.debug( 'remove-> Netted {} position #{} with {} trade #{} PnL = {:.4f}'. format(self.side, order.id, netting_order.side, netting_order.id, pnl)) return pnl
def flatten_inventory(self, price: float) -> float: """ Flatten all positions held in inventory. :param price: (float) current bid or ask price :return: (float) PnL from flattening inventory """ LOGGER.debug('{} is flattening inventory of {}'.format( self.side, self.position_count)) if self.position_count < 1: return -ENCOURAGEMENT pnl = 0. # Need to reverse the side to reflect the correct direction of # the flatten_order() side = 'long' if self.side == 'short' else 'short' while self.position_count > 0: order = MarketOrder(ccy=None, side=side, price=price) pnl += self.remove(netting_order=order) self.total_trade_count += 1 # Deduct transaction fee based on order type if self.transaction_fee: pnl -= MARKET_ORDER_FEE # Update statistics self.statistics.market_orders += 1 return pnl
def _add_market_order(self, order: MarketOrder) -> bool: """ Add a MARKET order. :param order: (Order) New order to be used for updating existing order or placing a new order """ if self.full_inventory: LOGGER.debug(' %s inventory max' % order.side) return False # Create a hypothetical average execution price incorporating a fixed slippage order.average_execution_price = order.price order.executed = order.DEFAULT_SIZE # Update position inventory attributes self.cancel_limit_order() # remove any unfilled limit orders self.positions.append(order) # execute and save the market order self.total_exposure += order.average_execution_price self.average_price = self.total_exposure / self.position_count self.full_inventory = self.position_count >= self.max_position_count self.total_trade_count += 1 # deduct transaction fees whenever an order gets filled if self.transaction_fee: self.realized_pnl -= MARKET_ORDER_FEE # update statistics self.statistics.market_orders += 1 LOGGER.debug(' %s @ %.2f | step %i' % (order.side, order.average_execution_price, order.step)) return True
def cancel_limit_order(self) -> bool: """ Cancel a limit order. :return: (bool) TRUE if cancel was successful """ if self.order is None: LOGGER.debug('No {} open orders to cancel.'.format(self.side)) return False LOGGER.debug('Cancelling order ({})'.format(self.order)) self.order = None return True
def flatten_inventory(self, bid_price: float, ask_price: float) -> float: """ Flatten all positions held in inventory. :param bid_price: (float) current bid price :param ask_price: (float) current ask price :return: (float) PnL from flattening inventory """ LOGGER.debug('Flattening inventory. {} longs / {} shorts'.format( self.long_inventory_count, self.short_inventory_count)) long_pnl = self.long_inventory.flatten_inventory(price=bid_price) short_pnl = self.short_inventory.flatten_inventory(price=ask_price) return long_pnl + short_pnl
def _add_limit_order(self, order: LimitOrder) -> bool: """ Add / update a LIMIT order. :param order: (Order) New order to be used for updating existing order or placing a new order """ if self.order is None: if self.full_inventory: LOGGER.debug( "{} order rejected. Already at max position limit ({})". format(self.side, self.max_position_count)) return False self.order = order # update statistics self.statistics.orders_placed += 1 LOGGER.debug('\nOpened new order={}'.format(order)) elif self.order.price != order.price: self.order.price = order.price self.order.queue_ahead = order.queue_ahead self.order.id = order.id self.order.step = order.step # update statistics self.statistics.orders_updated += 1 LOGGER.debug('\nUpdating order{} --> \n{}'.format( order, self.order)) else: LOGGER.debug("\nNothing to update about the order {}".format( self.order)) return True
def _step_limit_order(self, bid_price: float, ask_price: float, buy_volume: float, sell_volume: float, step: int) -> bool: """ Step in environment and update LIMIT order inventories. :param bid_price: best bid price :param ask_price: best ask price :param buy_volume: executions initiated by buyers (in notional terms) :param sell_volume: executions initiated by sellers (in notional terms) :param step: current time step :return: (bool) TRUE if a limit order was filled, otherwise FALSE """ if self.order is None: return False if self.order.side == 'long': if bid_price <= self.order.price: self._process_transaction_volume(volume=sell_volume) elif self.order.side == 'short': if ask_price >= self.order.price: self._process_transaction_volume(volume=buy_volume) if self.order.is_filled: avg_execution_px = self.order.get_average_execution_price() self.positions.append(self.order) self.total_exposure += avg_execution_px self.average_price = self.total_exposure / self.position_count self.full_inventory = self.position_count >= self.max_position_count self.total_trade_count += 1 LOGGER.debug( 'FILLED {} order #{} at {:.3f} after {} steps on {}.'.format( self.order.side, self.order.id, avg_execution_px, self.order.metrics.steps_in_position, step)) self.order = None # set the slot back to no open orders self.statistics.orders_executed += 1 # deduct transaction fees when the LIMIT order gets filled if self.transaction_fee: self.realized_pnl -= LIMIT_ORDER_FEE return True return False
def pop_position(self) -> LimitOrder: """ Remove LIMIT order position from inventory when netted out. :return: (LimitOrder) position being netted out """ if self.position_count > 0: position = self.positions.popleft() # update positions attributes self.total_exposure -= position.average_execution_price if self.position_count > 0: self.average_price = self.total_exposure / self.position_count else: self.average_price = 0 self.full_inventory = self.position_count >= self.max_position_count LOGGER.debug( 'pop_position-> %s position #%i @ %.4f has been netted out.' % (self.side, position.id, position.price)) return position else: raise ValueError('Error. No {} pop_position to remove.'.format( self.side))