def simulate(self, event, current_orders): dt = event.dt simulated_impact = 0.0 max_volume = self.volume_limit * event.volume total_volume = 0 txns = [] for order in current_orders: open_amount = order.amount - order.filled if zp_math.tolerant_equals(open_amount, 0): continue order.check_triggers(event) if not order.triggered: continue # price impact accounts for the total volume of transactions # created against the current minute bar remaining_volume = max_volume - total_volume if ( remaining_volume <= 0 or zp_math.tolerant_equals(remaining_volume, 0) ): # we can't fill any more transactions return txns # the current order amount will be the min of the # volume available in the bar or the open amount. cur_amount = min(remaining_volume, abs(open_amount)) cur_amount = cur_amount * order.direction # tally the current amount into our total amount ordered. # total amount will be used to calculate price impact total_volume = total_volume + order.direction * cur_amount volume_share = min(order.direction * (total_volume) / event.volume, self.volume_limit) simulated_impact = (volume_share) ** 2 \ * self.price_impact * order.direction * event.price if order.direction * cur_amount > 0: txn = create_transaction( event.sid, cur_amount, # In the future, we may want to change the next line # for limit pricing event.price + simulated_impact, dt.replace(tzinfo=pytz.utc), order.id ) txns.append(txn) return txns
def simulate(self, event, current_orders): dt = event.dt simulated_impact = 0.0 max_volume = self.volume_limit * event.volume total_volume = 0 txns = [] for order in current_orders: open_amount = order.amount - order.filled if zp_math.tolerant_equals(open_amount, 0): continue order.check_triggers(event) if not order.triggered: continue # price impact accounts for the total volume of transactions # created against the current minute bar remaining_volume = max_volume - total_volume if (remaining_volume <= 0 or zp_math.tolerant_equals(remaining_volume, 0)): # we can't fill any more transactions return txns # the current order amount will be the min of the # volume available in the bar or the open amount. cur_amount = min(remaining_volume, abs(open_amount)) cur_amount = cur_amount * order.direction # tally the current amount into our total amount ordered. # total amount will be used to calculate price impact total_volume = total_volume + order.direction * cur_amount volume_share = min(order.direction * (total_volume) / event.volume, self.volume_limit) simulated_impact = (volume_share) ** 2 \ * self.price_impact * order.direction * event.price if order.direction * cur_amount > 0: txn = create_transaction( event.sid, cur_amount, # In the future, we may want to change the next line # for limit pricing event.price + simulated_impact, dt.replace(tzinfo=pytz.utc), order.id) txns.append(txn) return txns
def asymmetric_round_price(price, prefer_round_down, tick_size, diff=0.95): """ Asymmetric rounding function for adjusting prices to the specified number of places in a way that "improves" the price. For limit prices, this means preferring to round down on buys and preferring to round up on sells. For stop prices, it means the reverse. If prefer_round_down == True: When .05 below to .95 above a specified decimal place, use it. If prefer_round_down == False: When .95 below to .05 above a specified decimal place, use it. In math-speak: If prefer_round_down: [<X-1>.0095, X.0195) -> round to X.01. If not prefer_round_down: (<X-1>.0005, X.0105] -> round to X.01. """ precision = zp_math.number_of_decimal_places(tick_size) multiplier = int(tick_size * (10**precision)) diff -= 0.5 # shift the difference down diff *= (10**-precision) # adjust diff to precision of tick size diff *= multiplier # adjust diff to value of tick_size # Subtracting an epsilon from diff to enforce the open-ness of the upper # bound on buys and the lower bound on sells. Using the actual system # epsilon doesn't quite get there, so use a slightly less epsilon-ey value. epsilon = float_info.epsilon * 10 diff = diff - epsilon # relies on rounding half away from zero, unlike numpy's bankers' rounding rounded = tick_size * consistent_round( (price - (diff if prefer_round_down else -diff)) / tick_size) if zp_math.tolerant_equals(rounded, 0.0): return 0.0 return rounded
def sharpe_ratio(algorithm_volatility, annualized_return, treasury_return): """ http://en.wikipedia.org/wiki/Sharpe_ratio Args: algorithm_volatility (float): Algorithm volatility. algorithm_return (float): Algorithm return percentage. treasury_return (float): Treasury return percentage. Returns: float. The Sharpe ratio. """ if zp_math.tolerant_equals(algorithm_volatility, 0): return np.nan return ( (annualized_return - treasury_return) # The square of the annualization factor is in the volatility, # because the volatility is also annualized, # i.e. the sqrt(annual factor) is in the volatility's numerator. # So to have the the correct annualization factor for the # Sharpe value's numerator, which should be the sqrt(annual factor). # The square of the sqrt of the annual factor, i.e. the annual factor # itself, is needed in the numerator to factor out the division by # its square root. / algorithm_volatility)
def process_trade(self, trade_event): if zp_math.tolerant_equals(trade_event.volume, 0): # there are zero volume trade_events bc some stocks trade # less frequently than once per minute. return [] if trade_event.sid in self.open_orders: orders = self.open_orders[trade_event.sid] orders = sorted(orders, key=lambda o: o.dt) # Only use orders for the current day or before current_orders = filter( lambda o: o.dt <= trade_event.dt, orders) else: return [] txns = self.transact(trade_event, current_orders) for txn in txns: self.txns_by_order[txn.order_id].append(txn) self.txns_by_sid[txn.sid].append(txn) self.orders[txn.order_id].filled += txn.amount # update the open orders for the trade_event's sid self.open_orders[trade_event.sid] = \ [order for order in orders if order.open] # drop any filled orders. filled = \ [order.id for order in orders if not order.open] for order_id in filled: del self.orders[order_id] return txns
def asymmetric_round_price_to_penny(price, prefer_round_down, diff=(0.0095 - .005)): """ Asymmetric rounding function for adjusting prices to two places in a way that "improves" the price. For limit prices, this means preferring to round down on buys and preferring to round up on sells. For stop prices, it means the reverse. If prefer_round_down == True: When .05 below to .95 above a penny, use that penny. If prefer_round_down == False: When .95 below to .05 above a penny, use that penny. In math-speak: If prefer_round_down: [<X-1>.0095, X.0195) -> round to X.01. If not prefer_round_down: (<X-1>.0005, X.0105] -> round to X.01. """ # Subtracting an epsilon from diff to enforce the open-ness of the upper # bound on buys and the lower bound on sells. Using the actual system # epsilon doesn't quite get there, so use a slightly less epsilon-ey value. epsilon = float_info.epsilon * 10 diff = diff - epsilon # relies on rounding half away from zero, unlike numpy's bankers' rounding rounded = round(price - (diff if prefer_round_down else -diff), 2) if zp_math.tolerant_equals(rounded, 0.0): return 0.0 return rounded
def information_ratio(algo_volatility, algorithm_return, benchmark_return): """ http://en.wikipedia.org/wiki/Information_ratio Args: algorithm_returns (np.array-like): All returns during algorithm lifetime. benchmark_returns (np.array-like): All benchmark returns during algo lifetime. Returns: float. Information ratio. """ if zp_math.tolerant_equals(algo_volatility, 0): return np.nan # The square of the annualization factor is in the volatility, # because the volatility is also annualized, # i.e. the sqrt(annual factor) is in the volatility's numerator. # So to have the the correct annualization factor for the # Sharpe value's numerator, which should be the sqrt(annual factor). # The square of the sqrt of the annual factor, i.e. the annual factor # itself, is needed in the numerator to factor out the division by # its square root. return (algorithm_return - benchmark_return) / algo_volatility
def process_trade(self, trade_event): if trade_event.type != zp.DATASOURCE_TYPE.TRADE: return if zp_math.tolerant_equals(trade_event.volume, 0): # there are zero volume trade_events bc some stocks trade # less frequently than once per minute. return if trade_event.sid not in self.open_orders: return orders = self.open_orders[trade_event.sid] orders = sorted(orders, key=lambda o: o.dt) # Only use orders for the current day or before current_orders = filter( lambda o: o.dt <= trade_event.dt, orders) for order, txn in self.process_transactions(trade_event, current_orders): yield order, txn # update the open orders for the trade_event's sid self.open_orders[trade_event.sid] = \ [order for order in self.open_orders[trade_event.sid] if order.open]
def simulate(self, event, orders): txns = [] for order in orders: # TODO: what if we have 2 orders, one for 100 shares long, # and one for 100 shares short # such as in a hedging scenario? # check price limits, continue if the # order isn't triggered yet order.check_triggers(event) if not order.triggered: continue if zp_math.tolerant_equals(order.amount, 0): return txns txn = create_transaction( event.sid, order.amount, event.price + (self.spread / 2.0 * order.direction), event.dt.replace(tzinfo=pytz.utc), order.id ) # mark the last_modified date of the order to match order.last_modified = event.dt txns.append(txn) return txns
def process_trade(self, trade_event): if zp_math.tolerant_equals(trade_event.volume, 0): # there are zero volume trade_events bc some stocks trade # less frequently than once per minute. return [], [] if trade_event.sid in self.open_orders: orders = self.open_orders[trade_event.sid] orders = sorted(orders, key=lambda o: o.dt) # Only use orders for the current day or before current_orders = filter(lambda o: o.dt <= trade_event.dt, orders) else: return [], [] txns = self.transact(trade_event, current_orders) for txn in txns: self.orders[txn.order_id].filled += txn.amount # mark the date of the order to match the txn self.orders[txn.order_id].dt = txn.dt modified_orders = [ order for order in self.open_orders[trade_event.sid] if order.dt == trade_event.dt ] for order in modified_orders: if not order.open: del self.orders[order.id] # update the open orders for the trade_event's sid self.open_orders[trade_event.sid] = \ [order for order in self.open_orders[trade_event.sid] if order.open] return txns, modified_orders
def assert_float_equal( result, expected, path=(), msg="", float_rtol=10e-7, float_atol=10e-7, float_equal_nan=True, **kwargs, ): assert tolerant_equals( result, expected, rtol=float_rtol, atol=float_atol, equal_nan=float_equal_nan, ), "%s%s != %s with rtol=%s and atol=%s%s\n%s" % ( _fmt_msg(msg), result, expected, float_rtol, float_atol, (" (with nan != nan)" if not float_equal_nan else ""), _fmt_path(path), )
def order_value(self, sid, value, limit_price=None, stop_price=None, style=None): """ Place an order by desired value rather than desired number of shares. If the requested sid is found in the universe, the requested value is divided by its price to imply the number of shares to transact. value > 0 :: Buy/Cover value < 0 :: Sell/Short Market order: order(sid, value) Limit order: order(sid, value, limit_price) Stop order: order(sid, value, None, stop_price) StopLimit order: order(sid, value, limit_price, stop_price) """ last_price = self.trading_client.current_data[sid].price if tolerant_equals(last_price, 0): zero_message = "Price of 0 for {psid}; can't infer value".format( psid=sid ) if self.logger: self.logger.debug(zero_message) # Don't place any order return else: amount = value / last_price return self.order(sid, amount, limit_price=limit_price, stop_price=stop_price, style=style)
def sharpe_ratio(algorithm_volatility, annualized_return, treasury_return): """ http://en.wikipedia.org/wiki/Sharpe_ratio Args: algorithm_volatility (float): Algorithm volatility. algorithm_return (float): Algorithm return percentage. treasury_return (float): Treasury return percentage. Returns: float. The Sharpe ratio. """ if zp_math.tolerant_equals(algorithm_volatility, 0): return np.nan return ( (annualized_return - treasury_return) # The square of the annualization factor is in the volatility, # because the volatility is also annualized, # i.e. the sqrt(annual factor) is in the volatility's numerator. # So to have the the correct annualization factor for the # Sharpe value's numerator, which should be the sqrt(annual factor). # The square of the sqrt of the annual factor, i.e. the annual factor # itself, is needed in the numerator to factor out the division by # its square root. / algorithm_volatility )
def information_ratio(algorithm_returns, benchmark_returns): """ http://en.wikipedia.org/wiki/Information_ratio Args: algorithm_returns (np.array-like): All returns during algorithm lifetime. benchmark_returns (np.array-like): All benchmark returns during algo lifetime. Returns: float. Information ratio. """ relative_returns = algorithm_returns - benchmark_returns relative_deviation = relative_returns.std(ddof=1) if ( zp_math.tolerant_equals(relative_deviation, 0) or np.isnan(relative_deviation) ): return 0.0 return np.mean(relative_returns) / relative_deviation
def sortino_ratio(algorithm_returns, algorithm_period_return, mar): """ http://en.wikipedia.org/wiki/Sortino_ratio Args: algorithm_returns (np.array-like): Returns from algorithm lifetime. algorithm_period_return (float): Algorithm return percentage from latest period. mar (float): Minimum acceptable return. Returns: float. The Sortino ratio. """ if len(algorithm_returns) == 0: return 0.0 rets = algorithm_returns downside = (rets[rets < mar] - mar)**2 dr = np.sqrt(downside.sum() / len(rets)) if zp_math.tolerant_equals(dr, 0): return 0.0 return (algorithm_period_return - mar) / dr
def information_ratio(algo_volatility, algorithm_return, benchmark_return): """ http://en.wikipedia.org/wiki/Information_ratio Args: algorithm_returns (np.array-like): All returns during algorithm lifetime. benchmark_returns (np.array-like): All benchmark returns during algo lifetime. Returns: float. Information ratio. """ if zp_math.tolerant_equals(algo_volatility, 0): return np.nan return ( (algorithm_return - benchmark_return) # The square of the annualization factor is in the volatility, # because the volatility is also annualized, # i.e. the sqrt(annual factor) is in the volatility's numerator. # So to have the the correct annualization factor for the # Sharpe value's numerator, which should be the sqrt(annual factor). # The square of the sqrt of the annual factor, i.e. the annual factor # itself, is needed in the numerator to factor out the division by # its square root. / algo_volatility )
def sortino_ratio(algorithm_returns, algorithm_period_return, mar): """ http://en.wikipedia.org/wiki/Sortino_ratio Args: algorithm_returns (np.array-like): Returns from algorithm lifetime. algorithm_period_return (float): Algorithm return percentage from latest period. mar (float): Minimum acceptable return. Returns: float. The Sortino ratio. """ if len(algorithm_returns) == 0: return 0.0 rets = algorithm_returns downside = (rets[rets < mar] - mar) ** 2 dr = np.sqrt(downside.sum() / len(rets)) if zp_math.tolerant_equals(dr, 0): return 0.0 return (algorithm_period_return - mar) / dr
def asymmetric_round_price(price, prefer_round_down, tick_size, diff=0.95): """ Asymmetric rounding function for adjusting prices to the specified number of places in a way that "improves" the price. For limit prices, this means preferring to round down on buys and preferring to round up on sells. For stop prices, it means the reverse. If prefer_round_down == True: When .05 below to .95 above a specified decimal place, use it. If prefer_round_down == False: When .95 below to .05 above a specified decimal place, use it. In math-speak: If prefer_round_down: [<X-1>.0095, X.0195) -> round to X.01. If not prefer_round_down: (<X-1>.0005, X.0105] -> round to X.01. """ precision = zp_math.number_of_decimal_places(tick_size) multiplier = int(tick_size * (10 ** precision)) diff -= 0.5 # shift the difference down diff *= (10 ** -precision) # adjust diff to precision of tick size diff *= multiplier # adjust diff to value of tick_size # Subtracting an epsilon from diff to enforce the open-ness of the upper # bound on buys and the lower bound on sells. Using the actual system # epsilon doesn't quite get there, so use a slightly less epsilon-ey value. epsilon = float_info.epsilon * 10 diff = diff - epsilon # relies on rounding half away from zero, unlike numpy's bankers' rounding rounded = tick_size * consistent_round( (price - (diff if prefer_round_down else -diff)) / tick_size ) if zp_math.tolerant_equals(rounded, 0.0): return 0.0 return rounded
def test_split_long_position(self): events = factory.create_trade_history(1, [20, 20], [100, 100], oneday, self.sim_params) # set up a long position in sid 1 # 100 shares at $20 apiece = $2000 position txns = [create_txn(events[0], 20, 100)] # set up a split with ratio 3 occurring at the start of the second # day. splits = [ factory.create_split( 1, 3, events[1].dt, ), ] results = calculate_results(self, events, txns=txns, splits=splits) # should have 33 shares (at $60 apiece) and $20 in cash self.assertEqual(2, len(results)) latest_positions = results[1]['daily_perf']['positions'] self.assertEqual(1, len(latest_positions)) # check the last position to make sure it's been updated position = latest_positions[0] self.assertEqual(1, position['sid']) self.assertEqual(33, position['amount']) self.assertEqual(60, position['cost_basis']) self.assertEqual(60, position['last_sale_price']) # since we started with $10000, and we spent $2000 on the # position, but then got $20 back, we should have $8020 # (or close to it) in cash. # we won't get exactly 8020 because sometimes a split is # denoted as a ratio like 0.3333, and we lose some digits # of precision. thus, make sure we're pretty close. daily_perf = results[1]['daily_perf'] self.assertTrue( zp_math.tolerant_equals(8020, daily_perf['ending_cash'], 1)) for i, result in enumerate(results): for perf_kind in ('daily_perf', 'cumulative_perf'): perf_result = result[perf_kind] # prices aren't changing, so pnl and returns should be 0.0 self.assertEqual( 0.0, perf_result['pnl'], "day %s %s pnl %s instead of 0.0" % (i, perf_kind, perf_result['pnl'])) self.assertEqual( 0.0, perf_result['returns'], "day %s %s returns %s instead of 0.0" % (i, perf_kind, perf_result['returns']))
def test_split_long_position(self): with trading.TradingEnvironment() as env: events = factory.create_trade_history( 1, [20, 20], [100, 100], oneday, self.sim_params ) # set up a long position in sid 1 # 100 shares at $20 apiece = $2000 position events.insert(0, create_txn(events[0], 20, 100)) # set up a split with ratio 3 events.append(factory.create_split(1, 3, env.next_trading_day(events[1].dt))) results = calculate_results(self, events) # should have 33 shares (at $60 apiece) and $20 in cash self.assertEqual(2, len(results)) latest_positions = results[1]['daily_perf']['positions'] self.assertEqual(1, len(latest_positions)) # check the last position to make sure it's been updated position = latest_positions[0] self.assertEqual(1, position['sid']) self.assertEqual(33, position['amount']) self.assertEqual(60, position['cost_basis']) self.assertEqual(60, position['last_sale_price']) # since we started with $10000, and we spent $2000 on the # position, but then got $20 back, we should have $8020 # (or close to it) in cash. # we won't get exactly 8020 because sometimes a split is # denoted as a ratio like 0.3333, and we lose some digits # of precision. thus, make sure we're pretty close. daily_perf = results[1]['daily_perf'] self.assertTrue( zp_math.tolerant_equals(8020, daily_perf['ending_cash'], 1)) for i, result in enumerate(results): for perf_kind in ('daily_perf', 'cumulative_perf'): perf_result = result[perf_kind] # prices aren't changing, so pnl and returns should be 0.0 self.assertEqual(0.0, perf_result['pnl'], "day %s %s pnl %s instead of 0.0" % (i, perf_kind, perf_result['pnl'])) self.assertEqual(0.0, perf_result['returns'], "day %s %s returns %s instead of 0.0" % (i, perf_kind, perf_result['returns']))
def transact_stub(slippage, commission, event, open_orders): """ This is intended to be wrapped in a partial, so that the slippage and commission models can be enclosed. """ for order, transaction in slippage(event, open_orders): if (transaction and not zp_math.tolerant_equals(transaction.amount, 0)): direction = math.copysign(1, transaction.amount) per_share, total_commission = commission.calculate(transaction) transaction.price = transaction.price + (per_share * direction) transaction.commission = total_commission yield order, transaction
def get_stddev(self): # Sample standard deviation is undefined for a single event or # no events. if len(self) <= 1: return None else: average = self.sum / len(self) s_squared = (self.sum_sqr - self.sum * average) \ / (len(self) - 1) if zp_math.tolerant_equals(0, s_squared): return 0.0 stddev = sqrt(s_squared) return stddev
def assert_float_equal( result, expected, path=(), msg="", float_rtol=10e-7, float_atol=10e-7, float_equal_nan=True, **kwargs ): assert tolerant_equals(result, expected, rtol=float_rtol, atol=float_atol, equal_nan=float_equal_nan), ( "%s%s != %s with rtol=%s and atol=%s%s\n%s" % ( _fmt_msg(msg), result, expected, float_rtol, float_atol, (" (with nan != nan)" if not float_equal_nan else ""), _fmt_path(path), ) )
def process_trade(self, trade_event): if trade_event.type != zp.DATASOURCE_TYPE.TRADE: return if zp_math.tolerant_equals(trade_event.volume, 0): # there are zero volume trade_events bc some stocks trade # less frequently than once per minute. return if trade_event.sid in self.open_orders: orders = self.open_orders[trade_event.sid] orders = sorted(orders, key=lambda o: o.dt) # Only use orders for the current day or before current_orders = filter( lambda o: o.dt <= trade_event.dt, orders) else: return for order, txn in self.transact(trade_event, current_orders): if txn.type == zp.DATASOURCE_TYPE.COMMISSION: order.commission = (order.commission or 0.0) + txn.cost else: if txn.amount == 0: raise zipline.errors.TransactionWithNoAmount(txn=txn) if math.copysign(1, txn.amount) != order.direction: raise zipline.errors.TransactionWithWrongDirection( txn=txn, order=order) if abs(txn.amount) > abs(self.orders[txn.order_id].amount): raise zipline.errors.TransactionVolumeExceedsOrder( txn=txn, order=order) order.filled += txn.amount if txn.commission is not None: order.commission = ((order.commission or 0.0) + txn.commission) # mark the date of the order to match the transaction # that is filling it. order.dt = txn.dt yield txn, order # update the open orders for the trade_event's sid self.open_orders[trade_event.sid] = \ [order for order in self.open_orders[trade_event.sid] if order.open]
def sharpe_ratio(algorithm_volatility, algorithm_return, treasury_return): """ http://en.wikipedia.org/wiki/Sharpe_ratio Args: algorithm_volatility (float): Algorithm volatility. algorithm_return (float): Algorithm return percentage. treasury_return (float): Treasury return percentage. Returns: float. The Sharpe ratio. """ if zp_math.tolerant_equals(algorithm_volatility, 0): return 0.0 return (algorithm_return - treasury_return) / algorithm_volatility
def _calculate_order_value_amount(self, asset, value): """ # on live trading the delisted check is delegated to the broker """ last_price = self.trading_client.current_data.current(asset, "price") if tolerant_equals(last_price, 0): zero_message = "Price of 0 for {psid}; can't infer value".format( psid=asset) if self.logger: self.logger.debug(zero_message) # Don't place any order return 0 value_multiplier = asset.price_multiplier return value / (last_price * value_multiplier)
def sharpe_ratio(algorithm_volatility, algorithm_return, treasury_return): """ http://en.wikipedia.org/wiki/Sharpe_ratio Args: algorithm_volatility (float): Algorithm volatility. algorithm_return (float): Algorithm return percentage. treasury_return (float): Treasury return percentage. Returns: float. The Sharpe ratio. """ if zp_math.tolerant_equals(algorithm_volatility, 0): return np.nan return (algorithm_return - treasury_return) / algorithm_volatility
def transact_stub(slippage, commission, event, open_orders): """ This is intended to be wrapped in a partial, so that the slippage and commission models can be enclosed. """ for order, transaction in slippage(event, open_orders): if ( transaction and not zp_math.tolerant_equals(transaction.amount, 0) ): direction = math.copysign(1, transaction.amount) per_share, total_commission = commission.calculate(transaction) transaction.price = transaction.price + (per_share * direction) transaction.commission = total_commission yield order, transaction
def test_split_long_position(self): with trading.TradingEnvironment() as env: events = factory.create_trade_history( 1, [20, 20], [100, 100], oneday, self.sim_params ) # set up a long position in sid 1 # 100 shares at $20 apiece = $2000 position events.insert(0, create_txn(events[0], 20, 100)) # set up a split with ratio 3 events.append(factory.create_split(1, 3, env.next_trading_day(events[1].dt))) results = calculate_results(self, events) # should have 33 shares (at $60 apiece) and $20 in cash self.assertEqual(2, len(results)) latest_positions = results[1]['daily_perf']['positions'] self.assertEqual(1, len(latest_positions)) # check the last position to make sure it's been updated position = latest_positions[0] self.assertEqual(1, position['sid']) self.assertEqual(33, position['amount']) self.assertEqual(60, position['cost_basis']) self.assertEqual(60, position['last_sale_price']) # since we started with $10000, and we spent $2000 on the # position, but then got $20 back, we should have $8020 # (or close to it) in cash. # we won't get exactly 8020 because sometimes a split is # denoted as a ratio like 0.3333, and we lose some digits # of precision. thus, make sure we're pretty close. daily_perf = results[1]['daily_perf'] self.assertTrue( zp_math.tolerant_equals(8020, daily_perf['ending_cash'], 1))
def process_trade(self, trade_event): if trade_event.type != zp.DATASOURCE_TYPE.TRADE: return if zp_math.tolerant_equals(trade_event.volume, 0): # there are zero volume trade_events bc some stocks trade # less frequently than once per minute. return if trade_event.sid in self.open_orders: orders = self.open_orders[trade_event.sid] orders = sorted(orders, key=lambda o: o.dt) # Only use orders for the current day or before current_orders = filter(lambda o: o.dt <= trade_event.dt, orders) else: return for order, txn in self.transact(trade_event, current_orders): if txn.type == zp.DATASOURCE_TYPE.COMMISSION: order.commission = (order.commission or 0.0) + txn.cost else: if txn.amount == 0: raise zipline.errors.TransactionWithNoAmount(txn=txn) if math.copysign(1, txn.amount) != order.direction: raise zipline.errors.TransactionWithWrongDirection( txn=txn, order=order) if abs(txn.amount) > abs(self.orders[txn.order_id].amount): raise zipline.errors.TransactionVolumeExceedsOrder( txn=txn, order=order) order.filled += txn.amount if txn.commission is not None: order.commission = ((order.commission or 0.0) + txn.commission) # mark the date of the order to match the transaction # that is filling it. order.dt = txn.dt yield txn, order # update the open orders for the trade_event's sid self.open_orders[trade_event.sid] = \ [order for order in self.open_orders[trade_event.sid] if order.open]
def sortino_ratio(annualized_algorithm_return, treasury_return, downside_risk): """ http://en.wikipedia.org/wiki/Sortino_ratio Args: algorithm_returns (np.array-like): Returns from algorithm lifetime. algorithm_period_return (float): Algorithm return percentage from latest period. mar (float): Minimum acceptable return. Returns: float. The Sortino ratio. """ if np.isnan(downside_risk) or zp_math.tolerant_equals(downside_risk, 0): return 0.0 return (annualized_algorithm_return - treasury_return) / downside_risk
def simulate(self, event, current_orders): self._volume_for_bar = 0 for order in current_orders: if zp_math.tolerant_equals(order.open_amount, 0): continue order.check_triggers(event) if not order.triggered: continue txn = self.process_order(event, order) if txn: self._volume_for_bar += abs(txn.amount) yield order, txn
def sortino_ratio(algorithm_period_return, treasury_period_return, mar): """ http://en.wikipedia.org/wiki/Sortino_ratio Args: algorithm_returns (np.array-like): Returns from algorithm lifetime. algorithm_period_return (float): Algorithm return percentage from latest period. mar (float): Minimum acceptable return. Returns: float. The Sortino ratio. """ if zp_math.tolerant_equals(mar, 0): return 0.0 return (algorithm_period_return - treasury_period_return) / mar
def _calculate_order_value_amount(self, asset, value): """ Calculates how many shares/contracts to order based on the type of asset being ordered. """ last_price = self.trading_client.current_data[asset].price if tolerant_equals(last_price, 0): zero_message = "Price of 0 for {psid}; can't infer value".format(psid=asset) if self.logger: self.logger.debug(zero_message) # Don't place any order return 0 if isinstance(asset, Future): value_multiplier = asset.contract_multiplier else: value_multiplier = 1 return value / (last_price * value_multiplier)
def _calculate_order_value_amount(self, asset, value): """ Calculates how many shares/contracts to order based on the type of asset being ordered. """ last_price = self.trading_client.current_data[asset].price if tolerant_equals(last_price, 0): zero_message = "Price of 0 for {psid}; can't infer value".format( psid=asset) if self.logger: self.logger.debug(zero_message) # Don't place any order return 0 if isinstance(asset, Future): value_multiplier = asset.contract_multiplier else: value_multiplier = 1 return value / (last_price * value_multiplier)
def order_target_value(self, sid, target, limit_price=None, stop_price=None, style=None): """ Place an order to adjust a position to a target value. If the position doesn't already exist, this is equivalent to placing a new order. If the position does exist, this is equivalent to placing an order for the difference between the target value and the current value. """ last_price = self.trading_client.current_data[sid].price if tolerant_equals(last_price, 0): # Don't place an order if self.logger: zero_message = "Price of 0 for {psid}; can't infer value" self.logger.debug(zero_message.format(psid=sid)) return target_amount = target / last_price return self.order_target(sid, target_amount, limit_price=limit_price, stop_price=stop_price, style=style)
def transact_stub(slippage, commission, event, open_orders): """ This is intended to be wrapped in a partial, so that the slippage and commission models can be enclosed. """ def inject_algo_dt(record): if not 'algo_dt' in record.extra: record.extra['algo_dt'] = event['dt'] with Processor(inject_algo_dt).threadbound(): transactions = slippage.simulate(event, open_orders) for transaction in transactions: if (transaction and not zp_math.tolerant_equals(transaction.amount, 0)): direction = math.copysign(1, transaction.amount) per_share, total_commission = commission.calculate(transaction) transaction.price = transaction.price + (per_share * direction) transaction.commission = total_commission return transactions
def information_ratio(algorithm_returns, benchmark_returns): """ http://en.wikipedia.org/wiki/Information_ratio Args: algorithm_returns (np.array-like): All returns during algorithm lifetime. benchmark_returns (np.array-like): All benchmark returns during algo lifetime. Returns: float. Information ratio. """ relative_returns = algorithm_returns - benchmark_returns relative_deviation = relative_returns.std(ddof=1) if zp_math.tolerant_equals(relative_deviation, 0) or \ np.isnan(relative_deviation): return 0.0 return np.mean(relative_returns) / relative_deviation
def assert_float_equal(result, expected, path=(), msg='', float_rtol=10e-7, float_atol=10e-7, float_equal_nan=True, **kwargs): assert tolerant_equals( result, expected, rtol=float_rtol, atol=float_atol, equal_nan=float_equal_nan, ), '%s%s != %s with rtol=%s and atol=%s%s\n%s' % ( _fmt_msg(msg), result, expected, float_rtol, float_atol, (' (with nan != nan)' if not float_equal_nan else ''), _fmt_path(path), )
def assert_float_equal(result, expected, path=(), msg='', float_rtol=10e-7, float_atol=10e-7, float_equal_nan=True, **kwargs): assert tolerant_equals( result, expected, rtol=float_rtol, atol=float_atol, equal_nan=float_equal_nan, ), '{}{} != {} with rtol={} and atol={}{}\n{}'.format( _fmt_msg(msg), result, expected, float_rtol, float_atol, (' (with nan != nan)' if not float_equal_nan else ''), _fmt_path(path), )
def simulate(self, event, current_orders): self._volume_for_bar = 0 txns = [] for order in current_orders: open_amount = order.amount - order.filled if zp_math.tolerant_equals(open_amount, 0): continue order.check_triggers(event) if not order.triggered: continue txn = self.process_order(event, order) if txn: txns.append(txn) self._volume_for_bar += math.copysign(txn.amount, 1) return txns
def process_trade(self, trade_event): if trade_event.type != zp.DATASOURCE_TYPE.TRADE: return [], [] if zp_math.tolerant_equals(trade_event.volume, 0): # there are zero volume trade_events bc some stocks trade # less frequently than once per minute. return [], [] if trade_event.sid in self.open_orders: orders = self.open_orders[trade_event.sid] orders = sorted(orders, key=lambda o: o.dt) # Only use orders for the current day or before current_orders = filter( lambda o: o.dt <= trade_event.dt, orders) else: return [], [] txns = self.transact(trade_event, current_orders) for txn in txns: self.orders[txn.order_id].filled += txn.amount # mark the date of the order to match the transaction # that is filling it. self.orders[txn.order_id].dt = txn.dt modified_orders = [order for order in self.open_orders[trade_event.sid] if order.dt == trade_event.dt] # update the open orders for the trade_event's sid self.open_orders[trade_event.sid] = \ [order for order in self.open_orders[trade_event.sid] if order.open] return txns, modified_orders
def process_order(self, event, order): max_volume = self.volume_limit * event.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 <= 0 or zp_math.tolerant_equals(remaining_volume, 0) ): # we can't fill any more transactions return # the current order amount will be the min of the # volume available in the bar or the open amount. cur_volume = min(remaining_volume, abs(order.open_amount)) # tally the current amount into our total amount ordered. # total amount will be used to calculate price impact total_volume = self.volume_for_bar + cur_volume volume_share = min(total_volume / event.volume, self.volume_limit) simulated_impact = (volume_share) ** 2 \ * math.copysign(self.price_impact, order.direction) \ * event.price return create_transaction( event, order, # In the future, we may want to change the next line # for limit pricing event.price + simulated_impact, math.copysign(cur_volume, order.direction) )
def transact_stub(slippage, commission, event, open_orders): """ This is intended to be wrapped in a partial, so that the slippage and commission models can be enclosed. """ def inject_algo_dt(record): if not 'algo_dt' in record.extra: record.extra['algo_dt'] = event['dt'] with Processor(inject_algo_dt).threadbound(): transactions = slippage.simulate(event, open_orders) for transaction in transactions: if ( transaction and not zp_math.tolerant_equals(transaction.amount, 0) ): direction = math.copysign(1, transaction.amount) per_share, total_commission = commission.calculate(transaction) transaction.price = transaction.price + (per_share * direction) transaction.commission = total_commission return transactions
def simulate(self, event, orders): txns = [] for order in orders: # TODO: what if we have 2 orders, one for 100 shares long, # and one for 100 shares short # such as in a hedging scenario? order.check_triggers(event) if not order.triggered: continue if zp_math.tolerant_equals(order.amount, 0): return txns txn = create_transaction( event.sid, order.amount, event.price + (self.spread / 2.0 * order.direction), event.dt.replace(tzinfo=pytz.utc), order.id) # mark the date of the order to match the transaction order.dt = event.dt txns.append(txn) return txns
def round_for_minimum_price_variation(x, is_buy, diff=(0.0095 - .005)): # relies on rounding half away from zero, unlike numpy's bankers' rounding rounded = round(x - (diff if is_buy else -diff), 2) if zp_math.tolerant_equals(rounded, 0.0): return 0.0 return rounded