def _generate_market_order(self, contract: Contract, signal: Signal): assert is_finite_number( self._initial_risk), "Initial risk has to be a finite number" assert is_finite_number(signal.fraction_at_risk ), "fraction_at_risk has to be a finite number" target_percentage = self._initial_risk / signal.fraction_at_risk self.logger.info("Target Percentage: {}".format(target_percentage)) target_percentage = self._cap_max_target_percentage(target_percentage) target_percentage *= signal.suggested_exposure.value # preserve the direction (-1, 0 , 1) self.logger.info("Target Percentage considering direction: {}".format( target_percentage)) assert is_finite_number( target_percentage), "target_percentage has to be a finite number" market_order_list = self._order_factory.target_percent_orders( {contract: target_percentage}, MarketOrder(), TimeInForce.OPG) if len(market_order_list) == 0: return None assert len( market_order_list) == 1, "Only one order should be generated" return market_order_list[0]
def _get_orders_with_fill_prices_without_slippage(self, market_orders_list, tickers, market_open, market_close): unique_tickers = list(set(tickers)) current_prices_series = self._get_current_prices(unique_tickers) expired_orders = [] # type: List[int] to_be_executed_orders = [] no_slippage_prices = [] # Check at first if at this moment of time, expiry checks should be made or not (optimization reasons) if market_open or market_close: # In case of market open or market close, some of the orders may expire for order, ticker in zip(market_orders_list, tickers): security_price = current_prices_series[ticker] if is_finite_number(security_price): to_be_executed_orders.append(order) no_slippage_prices.append(security_price) elif self._order_expires(order, market_open, market_close): expired_orders.append(order.id) else: for order, ticker in zip(market_orders_list, tickers): security_price = current_prices_series[ticker] if is_finite_number(security_price): to_be_executed_orders.append(order) no_slippage_prices.append(security_price) return no_slippage_prices, to_be_executed_orders, expired_orders
def update_price(self, bid_price: float, ask_price: float): """ Sets the current price of the security in a way that takes into account the bid-ask spread This is used for market valuation of the open position. This method should be called every time we have have a new price """ self._check_if_open() if self._quantity > 0 and is_finite_number( bid_price): # we are long -> use the lower (bid) price self._current_price = bid_price elif self._quantity < 0 and is_finite_number( ask_price): # we are short -> use the higher (ask) price self._current_price = ask_price
def _get_orders_with_fill_prices_without_slippage(self, market_orders_list, tickers): """ Unexecuted orders dict will always be empty. Executes only on the market open and drops the orders if unexecuted """ unique_tickers = list(set(tickers)) current_prices_series = self._data_handler.get_current_price( unique_tickers) unexecuted_orders_dict = {} # type: Dict[int, Order] to_be_executed_orders = [] no_slippage_prices = [] for order, ticker in zip(market_orders_list, tickers): security_price = current_prices_series[ticker] if is_finite_number(security_price): to_be_executed_orders.append(order) no_slippage_prices.append(security_price) else: if order.time_in_force == TimeInForce.GTC: # preserve only GTC orders. DAY orders will be dropped at this point unexecuted_orders_dict[order.id] = order return no_slippage_prices, to_be_executed_orders, unexecuted_orders_dict
def volume_weighted_average_price(prices_tms: PricesSeries, volumes_tms: QFSeries, interval: Timedelta) -> PricesSeries: """ Aggregates prices in the prices_tms by calculating the average weighted price for each period. The average weighted prices are weighted by the volumes traded in each period. Parameters ---------- prices_tms: PricesSeries timeseries of prices which should be aggregated volumes_tms: QFSeries timeseries of volumes traded; must correspond to the prices_tms interval: Timedelta the length of each period from which prices should be aggregated Returns ------- PricesSeries timeseries of aggregated prices; first datetimes are: first_price_datetime + interval, first_price_datetime + 2*interval, ..., first_price_datetime + i*interval, where first_price_datetime is the datetime of the first price in the original prices_tms The last datetime is always <= last datetime in the prices_tms """ assert prices_tms.index.equals(volumes_tms.index) last_date = prices_tms.index[-1] beginning_of_window = prices_tms.index[0] end_of_window = beginning_of_window + interval weighted_avg_price_tms = PricesSeries(name=prices_tms.name) while end_of_window < last_date: prices_in_window = prices_tms.loc[ beginning_of_window:end_of_window].drop([end_of_window]).values volumes_in_window = volumes_tms.loc[ beginning_of_window:end_of_window].drop([end_of_window]).values # if there are no prices in the window then skip try with the next window if prices_in_window.size == 0: continue # if there are no volumes to use -> assume that volume in each step is the same if count_nonzero(volumes_in_window) == 0: # if all the volumes are set to 0 than assume that volume for each asset is the same weighted_avg_price = mean(prices_in_window) else: # calculate volume-weighted average price weighted_price_sum = prices_in_window.dot(volumes_in_window) volume_sum = sum(volumes_in_window) weighted_avg_price = weighted_price_sum / volume_sum # if the weighted average price is equal exactly 0, it means that there were missing data if is_finite_number(weighted_avg_price) and weighted_avg_price != 0: weighted_avg_price_tms[end_of_window] = weighted_avg_price beginning_of_window = end_of_window end_of_window = end_of_window + interval return weighted_avg_price_tms
def execute_orders(self, market_open=False, market_close=False): """ Converts Orders into Transactions. Preserves the dictionary of unexecuted Orders (order_id -> Order) """ open_orders_list = self.get_open_orders() if not open_orders_list: return tickers = [order.ticker for order in open_orders_list] no_slippage_fill_prices_list, to_be_executed_orders, expired_orders_list = \ self._get_orders_with_fill_prices_without_slippage(open_orders_list, tickers, market_open, market_close) if len(to_be_executed_orders) > 0: current_time = self._timer.now() fill_prices, fill_volumes = self._slippage_model.process_orders( current_time, to_be_executed_orders, no_slippage_fill_prices_list) for order, fill_price, fill_volume in zip(to_be_executed_orders, fill_prices, fill_volumes): if fill_volume != 0 and is_finite_number(fill_price): self._execute_order(order, fill_price, fill_volume) # Delete the executed orders from awaiting orders dictionary del self._awaiting_orders[order.id] # If any orders have been executed - update the portfolio self._portfolio.update() # Delete all expired orders for expired_order_id in expired_orders_list: del self._awaiting_orders[expired_order_id]
def _generate_stop_order(self, signal, ticker_to_market_order: Dict[Ticker, Order]) -> Optional[Order]: """ As each of the stop orders relies on the precomputed stop_price, which considers a.o. last available price of the security, orders are being created separately for each of the signals. """ # stop_quantity = existing position size + recent market orders quantity stop_quantity = self._get_existing_position_quantity(signal.ticker) try: market_order = ticker_to_market_order[signal.ticker] stop_quantity += market_order.quantity except KeyError: # Generated Market Order was equal to None pass if stop_quantity != 0: stop_price = self._calculate_stop_price(signal) if not is_finite_number(stop_price): self.logger.info("Stop price should be a finite number") return None stop_price = self._cap_stop_price(stop_price, signal) # put minus before the quantity as stop order has to go in the opposite direction stop_orders = self._order_factory.orders({signal.ticker: -stop_quantity}, StopOrder(stop_price), TimeInForce.GTC) assert len(stop_orders) == 1, "Only one order should be generated" return stop_orders[0] else: # quantity is 0 - no need to place a stop order return None
def _adjust_quantity(self, order: Order, stop_order: Optional[Order], volume_df: QFDataFrame) -> \ Tuple[Order, Order]: """Returns order with adjusted quantity if applicable.""" ticker = self._contract_ticker_mapper.contract_to_ticker(order.contract) def average_past_volume(ticker: Ticker) -> Optional[float]: volume_series = volume_df[ticker] volume_series = volume_series.dropna() volume_series = volume_series[volume_series >= 0] return volume_series.mean() past_volume = average_past_volume(ticker) if is_finite_number(past_volume): volume_limit: int = math.floor(past_volume * self._volume_percentage_limit) # Check if the order quantity exceeds the limit if abs(order.quantity) > volume_limit: final_quantity = volume_limit * np.sign(order.quantity) adjustment_difference = final_quantity - order.quantity self.logger.info("{} VolumeOrdersFilter: Quantity change {} " "\n\tfinal quantity: {}".format(self._data_handler.timer.now(), order, final_quantity)) order.quantity = final_quantity if stop_order: # Adjust the corresponding stop order stop_order_final_quantity = stop_order.quantity - adjustment_difference self.logger.info("{} VolumeOrdersFilter: Quantity change {} " "\n\tfinal quantity: {}".format(self._data_handler.timer.now(), stop_order, final_quantity)) stop_order.quantity = stop_order_final_quantity return order, stop_order
def _compute_target_percentage(self, signal): assert is_finite_number(signal.fraction_at_risk ), "fraction_at_risk has to be a finite number" target_percentage = self._initial_risk / signal.fraction_at_risk self.logger.info("Target Percentage for {}: {}".format( signal.ticker, target_percentage)) target_percentage = self._cap_max_target_percentage(target_percentage) target_percentage *= signal.suggested_exposure.value # preserve the direction (-1, 0 , 1) self.logger.info( "Target Percentage considering direction for {}: {}".format( signal.ticker, target_percentage)) assert is_finite_number( target_percentage), "target_percentage has to be a finite number" return target_percentage
def _atr_fraction_at_risk(self, ticker: Ticker, time_period): try: current_time = self.data_handler.timer.now() data_frame = self.get_data(ticker.name, current_time) atr_value = self.compute_atr(data_frame) return atr_value * self.risk_estimation_factor if is_finite_number( atr_value) else None except NotEnoughDataException: return None
def _to_supported_type(self, value): if isinstance(value, (numpy.int64, numpy.int32)): return int(value) elif is_finite_number(value) or isinstance(value, datetime): return value elif isinstance(value, Ticker): return value.as_string() else: return str(value)
def _compute_target_value(self, signal: Signal, frequency=Frequency.DAILY) -> float: """ Caps the target value, so that according to historical volume data, the position will not exceed max_volume_percentage * mean volume within last 100 days. """ ticker: Ticker = signal.ticker portfolio_value = self._broker.get_portfolio_value() target_percentage = self._compute_target_percentage(signal) target_value = portfolio_value * target_percentage end_date = self._data_handler.timer.now() start_date = end_date - RelativeDelta(days=100) if isinstance(ticker, FutureTicker): # Check if a futures chain instance already exists for this ticker and create it if not # The default adjustment method will be taken (FuturesAdjustmentMethod.NTH_NEAREST) as the volume should # not be adjusted if ticker not in self._cached_futures_chains_dict.keys(): self._cached_futures_chains_dict[ticker] = FuturesChain( ticker, self._data_handler) volume_series: PricesSeries = self._cached_futures_chains_dict[ ticker].get_price(PriceField.Volume, start_date, end_date, frequency) else: volume_series: PricesSeries = self._data_handler.get_price( ticker, PriceField.Volume, start_date, end_date, frequency) mean_volume = volume_series.mean() specific_ticker = ticker.get_current_specific_ticker() if isinstance( ticker, FutureTicker) else ticker current_price = self._data_handler.get_last_available_price( specific_ticker, frequency) contract_size = ticker.point_value if isinstance(ticker, FutureTicker) else 1 divisor = current_price * contract_size quantity = target_value // divisor if abs(quantity) > mean_volume * self._max_volume_percentage: target_quantity = np.floor(mean_volume * self._max_volume_percentage) target_value = target_quantity * divisor * np.sign(quantity) self.logger.info( "InitialRiskWithVolumePositionSizer: {} - capping {}.\n" "Initial quantity: {}\n" "Reduced quantity: {}".format(self._data_handler.timer.now(), ticker.ticker, quantity, target_quantity)) assert is_finite_number( target_value), "target_value has to be a finite number" return target_value
def _calculate_stop_price(self, signal: Signal): current_price = signal.last_available_price assert is_finite_number(current_price), f"Signal generated for the {signal.symbol} does not contain " \ f"last_available_price. In order to use the Position Sizer with " \ f"stop_losses it is necessary for the signals to contain the last " \ f"available price." price_multiplier = 1 - signal.fraction_at_risk * signal.suggested_exposure.value stop_price = price_multiplier * current_price stop_price = self._round_stop_price(stop_price) return stop_price
def _compute_target_percentage(self, signal): if not is_finite_number( signal.fraction_at_risk) or signal.fraction_at_risk == 0.0: self.logger.warn( "Invalid Fraction at Risk = {} for {}. Setting target percentage = 0.0" .format(signal.fraction_at_risk, signal.ticker)) return 0.0 target_percentage = self._initial_risk / signal.fraction_at_risk self.logger.info("Target Percentage for {}: {}".format( signal.ticker, target_percentage)) target_percentage = self._cap_max_target_percentage(target_percentage) target_percentage *= signal.suggested_exposure.value # preserve the direction (-1, 0 , 1) self.logger.info( "Target Percentage considering direction for {}: {}".format( signal.ticker, target_percentage)) assert is_finite_number( target_percentage), "target_percentage has to be a finite number" return target_percentage
def _cap_stop_price(self, stop_price: float, signal: Signal): """ Prevent the stop price from moving down in case of a long position or up in case of a short position. Adjust the stop price only in case if there is an existing open position for specifically this contract. E.g. in case if there exists an open position for the December Cotton future contract, which expired today, if we are creating an order for the January Cotton contract we should not adjust the StopOrders stop price. """ # If there exist an open position for the contract - check the previous Stop Order specific_ticker = signal.ticker.get_current_specific_ticker() \ if isinstance(signal.ticker, FutureTicker) else signal.ticker contract = self._contract_ticker_mapper.ticker_to_contract( specific_ticker) position_quantity = self._get_existing_position_quantity(contract) if position_quantity != 0: # Get the last signal that was generated for the contract signals_series = self._signals_register.get_signals_for_ticker( ticker=specific_ticker, alpha_model=signal.alpha_model) signals_series = signals_series[ signals_series.index < self._data_handler.timer.now()] if not signals_series.empty: # Get the last signal, which was generated for the same exposure as the current exposure exposure_series = signals_series.apply( lambda s: s.suggested_exposure) current_exposure = Exposure.LONG if position_quantity > 0 else Exposure.SHORT last_date_with_current_exposure = exposure_series.where(exposure_series == current_exposure)\ .last_valid_index() if last_date_with_current_exposure is None: self.logger.error( "There is an open position for the contract {} but no signal was found" ) return stop_price last_signal_for_the_contract = signals_series.loc[ last_date_with_current_exposure] # type: Signal previous_stop_price = self._calculate_stop_price( last_signal_for_the_contract) if last_signal_for_the_contract.suggested_exposure == Exposure.OUT or \ not is_finite_number(previous_stop_price): return stop_price if last_signal_for_the_contract.suggested_exposure == Exposure.LONG: stop_price = max([stop_price, previous_stop_price]) elif last_signal_for_the_contract.suggested_exposure == Exposure.SHORT: stop_price = min([stop_price, previous_stop_price]) return stop_price
def _generate_exposure_values(self, config: FastAlphaModelTesterConfig, data_handler: FastDataHandler, tickers: Sequence[Ticker]): """ For the given Alpha model and its parameters, generates the dataframe containing all exposure values, that will be returned by the model through signals. """ model = config.generate_model(data_handler) current_exposures_values = QFSeries( index=pd.Index(tickers, name=TICKERS)) current_exposures_values[:] = 0.0 backtest_dates = pd.date_range(self._start_date, self._end_date, freq="B") exposure_values_df = QFDataFrame(index=backtest_dates, columns=pd.Index(tickers, name=TICKERS)) for ticker in tickers: if isinstance(ticker, FutureTicker): # Even if the tickers were already initialize, during pickling process, the data handler and timer # information is lost ticker.initialize_data_provider(self._timer, data_handler) for i, curr_datetime in enumerate(backtest_dates): new_exposures = QFSeries(index=tickers) self._timer.set_current_time(curr_datetime) for j, ticker, curr_exp_value in zip(count(), tickers, current_exposures_values): curr_exp = Exposure(curr_exp_value) if is_finite_number( curr_exp_value) else None try: new_exp = model.calculate_exposure(ticker, curr_exp) except NoValidTickerException: new_exp = None new_exposures.iloc[ j] = new_exp.value if new_exp is not None else None # assuming that we always follow the new_exposures from strategy, disregarding confidence levels # and expected moves, looking only at the suggested exposure current_exposures_values = new_exposures exposure_values_df.iloc[i, :] = current_exposures_values.iloc[:] exposure_values_df = exposure_values_df.dropna(axis=1, how="all") return exposure_values_df
def test_is_finite_number(self): self.assertFalse(is_finite_number(None)) self.assertFalse( is_finite_number( "There is a bomb attack on US president being planned")) self.assertFalse(is_finite_number(np.inf)) self.assertFalse(is_finite_number(np.nan)) self.assertFalse(is_finite_number(self)) self.assertTrue(is_finite_number(2.5))
def capture_ratio(strategy: QFSeries, benchmark: QFSeries, upside_capture: bool, frequency: Frequency = Frequency.MONTHLY) -> float: """ Upside / Downside Capture Ratio This ratio is a statistical measure of a Fund Manager's overall performance in down or up markets. For example Downside Capture Ratio is used to evaluate how well or poorly the Manager performed relative to a specific index during periods when that index has dropped (had negative returns). The ratio is calculated by dividing the Strategy returns by the returns of the index during the down-market Downside Capture Ratio = sum(Strategy Returns) / sum(Index Returns) A Fund Manager who has a downside capture ratio less than 1 has outperformed the index during the down-market by falling less than the index. For instance, a ratio of 0.75 indicates that the portfolio declined only 75% as much as the index during the period under consideration. Parameters ---------- strategy: series of the strategy that will be evaluated benchmark: series of the benchmark for the strategy upside_capture: bool True - for upside capture ratio False - for downside capture ratio frequency: Frequency Frequency on which the the ratio is evaluated. For example Frequency.MONTHLY will result in evaluating the ration based on Monthly returns. Returns ------- float """ aggregated_strategy = get_aggregate_returns(strategy, frequency) aggregated_benchmark = get_aggregate_returns(benchmark, frequency) if upside_capture: selected_dates = aggregated_benchmark.loc[aggregated_benchmark > 0].index else: selected_dates = aggregated_benchmark.loc[aggregated_benchmark < 0].index selected_strategy_returns = aggregated_strategy.loc[selected_dates] selected_benchmark_returns = aggregated_benchmark.loc[selected_dates] benchmark_sum = selected_benchmark_returns.sum() if benchmark_sum != 0.0 and is_finite_number(benchmark_sum): return selected_strategy_returns.sum() / selected_benchmark_returns.sum() return float('nan')
def __init__(self, broker: Broker, data_provider: DataProvider, order_factory: OrderFactory, signals_register: SignalsRegister, initial_risk: float, max_target_percentage: float = None, tolerance_percentage: float = 0.0): super().__init__(broker, data_provider, order_factory, signals_register) assert is_finite_number( initial_risk), "Initial risk has to be a finite number" assert initial_risk >= 0, "Initial risk has to be positive" self._initial_risk = initial_risk self.max_target_percentage = max_target_percentage self.tolerance_percentage = tolerance_percentage
def __init__(self, broker: Broker, data_handler: DataHandler, order_factory: OrderFactory, contract_ticker_mapper: ContractTickerMapper, signals_register: SignalsRegister, initial_risk: float, max_target_percentage: float = None, tolerance_percentage: float = 0.0): super().__init__(broker, data_handler, order_factory, contract_ticker_mapper, signals_register) assert is_finite_number( initial_risk), "Initial risk has to be a finite number" assert initial_risk >= 0, "Initial risk has to be positive" self._initial_risk = initial_risk self.max_target_percentage = max_target_percentage self.tolerance_percentage = tolerance_percentage self._signals_register.set_initial_risk(initial_risk) self.logger.info("Initial Risk: {}".format(initial_risk))
def _generate_stop_order(self, contract, signal, market_order: Order): # stop_quantity = existing position size + recent market orders quantity stop_quantity = self._get_existing_position_quantity(contract) if market_order is not None: stop_quantity += market_order.quantity if stop_quantity != 0: stop_price = self._calculate_stop_price(signal) assert is_finite_number( stop_price), "Stop price should be a finite number" # put minus before the quantity as stop order has to go in the opposite direction stop_orders = self._order_factory.orders( {contract: -stop_quantity}, StopOrder(stop_price), TimeInForce.GTC) assert len(stop_orders) == 1, "Only one order should be generated" return stop_orders[0] else: # quantity is 0 - no need to place a stop order return None
def append_to_statistics(measure_description: str, function: Callable, trades_containers, percentage_style: bool = False): style_format = "{:.2%}" if percentage_style else "{:.2f}" returned_values = (function(tc) for tc in trades_containers) returned_values = (value if is_finite_number(value) else 0.0 for value in returned_values) statistics.append((measure_description, *(style_format.format(val) for val in returned_values)))