class OasisMarketMakerChart: """Tool to analyze the OasisDEX Market Maker keeper performance.""" def __init__(self, args: list): parser = argparse.ArgumentParser(prog='oasis-market-maker-chart') parser.add_argument("--rpc-host", help="JSON-RPC host (default: `localhost')", default="localhost", type=str) parser.add_argument("--rpc-port", help="JSON-RPC port (default: `8545')", default=8545, type=int) parser.add_argument("--oasis-address", help="Ethereum address of the OasisDEX contract", required=True, type=str) parser.add_argument("--sai-address", help="Ethereum address of the SAI token", required=True, type=str) parser.add_argument("--weth-address", help="Ethereum address of the WETH token", required=True, type=str) parser.add_argument( "--market-maker-address", help="Ethereum account of the market maker to analyze", required=True, type=str) parser.add_argument("--past-blocks", help="Number of past blocks to analyze", required=True, type=int) parser.add_argument("-o", "--output", help="Name of the filename to save to chart to." " Will get displayed on-screen if empty", required=False, type=str) self.arguments = parser.parse_args(args) self.web3 = Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={'timeout': 120})) self.sai_address = Address(self.arguments.sai_address) self.weth_address = Address(self.arguments.weth_address) self.market_maker_address = Address( self.arguments.market_maker_address) self.otc = SimpleMarket(web3=self.web3, address=Address(self.arguments.oasis_address)) if self.arguments.output: import matplotlib matplotlib.use('Agg') def main(self): past_make = self.otc.past_make(self.arguments.past_blocks) past_take = self.otc.past_take(self.arguments.past_blocks) past_kill = self.otc.past_kill(self.arguments.past_blocks) def reduce_func(states, timestamp): if len(states) == 0: order_book = [] else: order_book = states[-1].order_book # apply all LogMake events having this timestamp for log_make in filter( lambda log_make: log_make.timestamp == timestamp, past_make): order_book = self.apply_make(order_book, log_make) order_book = list( filter( lambda order: order.maker == self.market_maker_address, order_book)) # apply all LogTake events having this timestamp for log_take in filter( lambda log_take: log_take.timestamp == timestamp, past_take): order_book = self.apply_take(order_book, log_take) # apply all LogKill events having this timestamp for log_kill in filter( lambda log_kill: log_kill.timestamp == timestamp, past_kill): order_book = self.apply_kill(order_book, log_kill) return states + [ State(timestamp=timestamp, order_book=order_book, market_price=None, sai_address=self.sai_address, weth_address=self.weth_address) ] def sell_trades() -> List[Trade]: regular = map( lambda log_take: Trade( log_take.timestamp, log_take.give_amount / log_take. take_amount, log_take.give_amount, False, True), filter( lambda log_take: log_take.maker == self. market_maker_address and log_take.buy_token == self. sai_address and log_take.pay_token == self.weth_address, past_take)) matched = map( lambda log_take: Trade( log_take.timestamp, log_take.take_amount / log_take. give_amount, log_take.take_amount, False, True), filter( lambda log_take: log_take.taker == self. market_maker_address and log_take.buy_token == self. weth_address and log_take.pay_token == self.sai_address, past_take)) return list(regular) + list(matched) def buy_trades() -> List[Trade]: regular = map( lambda log_take: Trade( log_take.timestamp, log_take.take_amount / log_take. give_amount, log_take.take_amount, True, False), filter( lambda log_take: log_take.maker == self. market_maker_address and log_take.buy_token == self. weth_address and log_take.pay_token == self.sai_address, past_take)) matched = map( lambda log_take: Trade( log_take.timestamp, log_take.give_amount / log_take. take_amount, log_take.give_amount, True, False), filter( lambda log_take: log_take.taker == self. market_maker_address and log_take.buy_token == self. sai_address and log_take.pay_token == self.weth_address, past_take)) return list(regular) + list(matched) event_timestamps = sorted( set( map(lambda event: event.timestamp, past_make + past_take + past_kill))) oasis_states = reduce(reduce_func, event_timestamps, []) gdax_states = self.get_gdax_states(event_timestamps) states = sorted(oasis_states + gdax_states, key=lambda state: state.timestamp) states = self.consolidate_states(states) trades = sell_trades() + buy_trades() self.draw(states, trades) def consolidate_states(self, states): last_market_price = None last_order_book = [] for i in range(0, len(states)): state = states[i] if state.order_book is None: state.order_book = last_order_book if state.market_price is None: state.market_price = last_market_price last_order_book = state.order_book last_market_price = state.market_price return states def get_gdax_states(self, timestamps: List[int]): first_timestamp = timestamps[0] last_timestamp = max(timestamps[-1], int(time.time())) states = [] timestamp = first_timestamp while timestamp <= last_timestamp: timestamp_range_start = timestamp timestamp_range_end = int( (datetime.datetime.fromtimestamp(timestamp) + datetime.timedelta(hours=3)).timestamp()) states = states + list( filter( lambda state: state.timestamp >= first_timestamp and state. timestamp <= last_timestamp, self.get_gdax_partial(timestamp_range_start, timestamp_range_end))) timestamp = timestamp_range_end return states def get_gdax_partial(self, timestamp_range_start: int, timestamp_range_end: int): start = datetime.datetime.fromtimestamp(timestamp_range_start, pytz.UTC) end = datetime.datetime.fromtimestamp(timestamp_range_end, pytz.UTC) url = f"https://api.gdax.com/products/ETH-USD/candles?" \ f"start={iso_8601(start)}&" \ f"end={iso_8601(end)}&" \ f"granularity=60" print(f"Downloading: {url}") # data is: [[ time, low, high, open, close, volume ], [...]] try: data = requests.get(url).json() except: print("GDAX API network error, waiting 10 secs...") time.sleep(10) return self.get_gdax_partial(timestamp_range_start, timestamp_range_end) if 'message' in data: print("GDAX API rate limiting, slowing down for 2 secs...") time.sleep(2) return self.get_gdax_partial(timestamp_range_start, timestamp_range_end) else: return list( map( lambda array: State( timestamp=array[0], order_book=None, market_price=array[3], # array[3] is 'open' sai_address=self.sai_address, weth_address=self.weth_address), data)) def convert_timestamp(self, timestamp): from matplotlib.dates import date2num return date2num(datetime.datetime.fromtimestamp(timestamp)) def to_size(self, trade: Trade): return amount_in_sai_to_size(trade.value_in_sai) def draw(self, states: List[State], trades: List[Trade]): import matplotlib.dates as md import matplotlib.pyplot as plt plt.subplots_adjust(bottom=0.2) plt.xticks(rotation=25) ax = plt.gca() ax.xaxis.set_major_formatter(md.DateFormatter('%Y-%m-%d %H:%M:%S')) timestamps = list( map(self.convert_timestamp, map(lambda state: state.timestamp, states))) closest_sell_prices = list( map(lambda state: state.closest_sell_price(), states)) closest_buy_prices = list( map(lambda state: state.closest_buy_price(), states)) market_prices = list(map(lambda state: state.market_price, states)) plt.plot_date(timestamps, closest_sell_prices, 'b-', zorder=1) plt.plot_date(timestamps, closest_buy_prices, 'g-', zorder=1) plt.plot_date(timestamps, market_prices, 'r-', zorder=1) sell_trades = list(filter(lambda trade: trade.is_sell, trades)) sell_x = list( map(self.convert_timestamp, map(lambda trade: trade.timestamp, sell_trades))) sell_y = list(map(lambda trade: trade.price, sell_trades)) sell_s = list(map(self.to_size, sell_trades)) plt.scatter(x=sell_x, y=sell_y, s=sell_s, c='blue', zorder=2) buy_trades = list(filter(lambda trade: trade.is_buy, trades)) buy_x = list( map(self.convert_timestamp, map(lambda trade: trade.timestamp, buy_trades))) buy_y = list(map(lambda trade: trade.price, buy_trades)) buy_s = list(map(self.to_size, buy_trades)) plt.scatter(x=buy_x, y=buy_y, s=buy_s, c='green', zorder=2) if self.arguments.output: plt.savefig(fname=self.arguments.output, dpi=300, bbox_inches='tight', pad_inches=0) else: plt.show() def apply_make(self, order_book: List[Order], log_make: LogMake) -> List[Order]: return order_book + [ Order(self.otc, order_id=log_make.order_id, pay_amount=log_make.pay_amount, pay_token=log_make.pay_token, buy_amount=log_make.buy_amount, buy_token=log_make.buy_token, maker=log_make.maker, timestamp=log_make.timestamp) ] def apply_take(self, order_book: List[Order], log_take: LogTake): this_order = next( filter(lambda order: order.order_id == log_take.order_id, order_book), None) if this_order is not None: assert this_order.pay_token == log_take.pay_token assert this_order.buy_token == log_take.buy_token remaining_orders = list( filter(lambda order: order.order_id != log_take.order_id, order_book)) this_order = Order( self.otc, order_id=this_order.order_id, pay_amount=this_order.pay_amount - log_take.take_amount, pay_token=this_order.pay_token, buy_amount=this_order.buy_amount - log_take.give_amount, buy_token=this_order.buy_token, maker=this_order.maker, timestamp=this_order.timestamp) if this_order.pay_amount > Wad(0) and this_order.buy_amount > Wad( 0): return remaining_orders + [this_order] else: return remaining_orders else: return order_book def apply_kill(self, order_book: List[Order], log_kill: LogKill) -> List[Order]: return list( filter(lambda order: order.order_id != log_kill.order_id, order_book))
class OasisMarketMakerChart: """Tool to generate a chart displaying the OasisDEX market maker keeper trades.""" def __init__(self, args: list): parser = argparse.ArgumentParser(prog='oasis-market-maker-chart') parser.add_argument("--rpc-host", help="JSON-RPC host (default: `localhost')", default="localhost", type=str) parser.add_argument("--rpc-port", help="JSON-RPC port (default: `8545')", default=8545, type=int) parser.add_argument("--rpc-timeout", help="JSON-RPC timeout (in seconds, default: 60)", type=int, default=60) parser.add_argument("--oasis-address", help="Ethereum address of the OasisDEX contract", required=True, type=str) parser.add_argument("--buy-token", help="Name of the buy token", required=True, type=str) parser.add_argument("--buy-token-address", help="Ethereum address of the buy token", required=True, type=str) parser.add_argument("--sell-token", help="Name of the sell token", required=True, type=str) parser.add_argument("--sell-token-address", help="Ethereum address of the sell token", required=True,type=str) parser.add_argument("--market-maker-address", help="Ethereum account of the market maker to analyze", required=True, type=str) parser.add_argument("--gdax-price", help="GDAX product (ETH-USD, BTC-USD) to use as the price history source", type=str) parser.add_argument("--price-feed", help="Price endpoint to use as the price history source", type=str) parser.add_argument("--alternative-price-feed", help="Price endpoint to use as the alternative price history source", type=str) parser.add_argument("--past-blocks", help="Number of past blocks to analyze", required=True, type=int) parser.add_argument("-o", "--output", help="Name of the filename to save to chart to." " Will get displayed on-screen if empty", required=False, type=str) self.arguments = parser.parse_args(args) self.web3 = Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={'timeout': self.arguments.rpc_timeout})) self.infura = Web3(HTTPProvider(endpoint_uri=f"https://mainnet.infura.io/", request_kwargs={'timeout': 120})) self.buy_token_address = Address(self.arguments.buy_token_address) self.sell_token_address = Address(self.arguments.sell_token_address) self.market_maker_address = Address(self.arguments.market_maker_address) self.otc = SimpleMarket(web3=self.web3, address=Address(self.arguments.oasis_address)) initialize_charting(self.arguments.output) initialize_logging() def main(self): start_timestamp = get_block_timestamp(self.infura, self.web3.eth.blockNumber - self.arguments.past_blocks) end_timestamp = int(time.time()) # If we only fetch log events from the last `past_blocks` blocks, the left hand side of the chart # will have some bid and ask lines missing as these orders were very likely created some blocks # earlier. So we also retrieve events from the blocks from the 24h before in order to minimize # the chance of it happening. block_lookback = 15*60*24 past_make = self.otc.past_make(self.arguments.past_blocks + block_lookback) past_take = self.otc.past_take(self.arguments.past_blocks + block_lookback) past_kill = self.otc.past_kill(self.arguments.past_blocks + block_lookback) def reduce_func(states, timestamp): if len(states) == 0: order_book = [] else: order_book = states[-1].order_book # apply all LogMake events having this timestamp for log_make in filter(lambda log_make: log_make.timestamp == timestamp, past_make): order_book = self.apply_make(order_book, log_make) order_book = list(filter(lambda order: order.maker == self.market_maker_address, order_book)) # apply all LogTake events having this timestamp for log_take in filter(lambda log_take: log_take.timestamp == timestamp, past_take): order_book = self.apply_take(order_book, log_take) # apply all LogKill events having this timestamp for log_kill in filter(lambda log_kill: log_kill.timestamp == timestamp, past_kill): order_book = self.apply_kill(order_book, log_kill) return states + [State(timestamp=timestamp, order_book=order_book, buy_token_address=self.buy_token_address, sell_token_address=self.sell_token_address)] event_timestamps = sorted(set(map(lambda event: event.timestamp, past_make + past_take + past_kill))) states_timestamps = self.tighten_timestamps(event_timestamps) + [end_timestamp] states = list(filter(lambda state: state.timestamp >= start_timestamp, reduce(reduce_func, states_timestamps, []))) states = sorted(states, key=lambda state: state.timestamp) prices = get_prices(self.arguments.gdax_price, self.arguments.price_feed, None, start_timestamp, end_timestamp) alternative_prices = get_prices(None, self.arguments.alternative_price_feed, None, start_timestamp, end_timestamp) takes = list(filter(lambda log_take: log_take.timestamp >= start_timestamp, past_take)) pair = self.arguments.sell_token + "-" + self.arguments.buy_token our_trades = our_oasis_trades(self.market_maker_address, self.buy_token_address, self.sell_token_address, takes, pair) all_trades = all_oasis_trades(self.buy_token_address, self.sell_token_address, takes, pair) draw_chart(start_timestamp, end_timestamp, prices, alternative_prices, 180, states, our_trades, all_trades, self.arguments.output) def tighten_timestamps(self, timestamps: list) -> list: if len(timestamps) == 0: return [] result = [timestamps[0]] for index in range(1, len(timestamps)): last_ts = timestamps[index-1] while True: last_ts += 60 if last_ts >= timestamps[index]: break result.append(last_ts) result.append(timestamps[index]) return result def apply_make(self, order_book: List[Order], log_make: LogMake) -> List[Order]: return order_book + [Order(self.otc, order_id=log_make.order_id, pay_amount=log_make.pay_amount, pay_token=log_make.pay_token, buy_amount=log_make.buy_amount, buy_token=log_make.buy_token, maker=log_make.maker, timestamp=log_make.timestamp)] def apply_take(self, order_book: List[Order], log_take: LogTake): this_order = next(filter(lambda order: order.order_id == log_take.order_id, order_book), None) if this_order is not None: assert this_order.pay_token == log_take.pay_token assert this_order.buy_token == log_take.buy_token remaining_orders = list(filter(lambda order: order.order_id != log_take.order_id, order_book)) this_order = Order(self.otc, order_id=this_order.order_id, pay_amount=this_order.pay_amount - log_take.take_amount, pay_token=this_order.pay_token, buy_amount=this_order.buy_amount - log_take.give_amount, buy_token=this_order.buy_token, maker=this_order.maker, timestamp=this_order.timestamp) if this_order.pay_amount > Wad(0) and this_order.buy_amount > Wad(0): return remaining_orders + [this_order] else: return remaining_orders else: return order_book def apply_kill(self, order_book: List[Order], log_kill: LogKill) -> List[Order]: return list(filter(lambda order: order.order_id != log_kill.order_id, order_book))