def bid(self, price: Dec, quantity: Dec, agent: "ag.MarketPlayer") -> Optional[Bid]: """ Submit a new sell order to the book. """ quantity = HavvenManager.round_decimal(quantity) # Disallow empty orders. if quantity == Dec(0): return None # Compute the fee to be paid. fee = self.buyer_fee(price, quantity) # Fail if the value of the order exceeds the agent's available supply. agent.round_values() if agent.__getattribute__( f"available_{self.quoted}") < HavvenManager.round_decimal( price * quantity) + fee: return None bid = Bid(price, quantity, fee, agent, self) # Attempt to trade the bid immediately. if self.continuous_order_matching: self.match() return bid
def step(self) -> None: # keep all havvens escrowed to make issuing nomins easier if self.available_havvens > 0: self.escrow_havvens(self.available_havvens) nomins = self.available_nomins + self.remaining_issuance_rights() if nomins > 0: trade = self._find_best_nom_fiat_trade() last_price = 0 while trade is not None and HavvenManager.round_decimal(nomins) > 0: if last_price == trade[0]: break last_price = trade[0] self._issue_nomins_up_to(trade[1]) ask = self._make_nom_fiat_trade(trade) trade = self._find_best_nom_fiat_trade() nomins = self.available_nomins + self.remaining_issuance_rights() if self.available_fiat > 0: trade = self._find_best_fiat_nom_trade() last_price = 0 while trade is not None and HavvenManager.round_decimal(self.available_fiat) > 0: if last_price == trade[0]: break last_price = trade[0] bid = self._make_fiat_nom_trade(trade) trade = self._find_best_fiat_nom_trade() if self.issued_nomins: if self.available_nomins < self.issued_nomins: self.burn_nomins(self.available_nomins) else: self.burn_nomins(self.issued_nomins)
def round_values(self) -> None: """ Apply rounding to this player's nomin, fiat, havven values. """ self.nomins = hm.round_decimal(self.nomins) self.fiat = hm.round_decimal(self.fiat) self.havvens = hm.round_decimal(self.havvens)
def profit_fraction(self) -> Dec: """ Return profit accrued as a fraction of initial wealth. May be negative. """ if hm.round_decimal(self.initial_wealth) != 0: return hm.round_decimal(self.profit() / self.initial_wealth) else: return Dec('0')
def step(self) -> None: if self.nomin_havven_order is not None: if self.model.manager.time >= self.nomin_havven_order[ 0] + self.trade_duration: self.nomin_havven_order[1].cancel() self.nomin_havven_order = None if self.nomin_fiat_order is not None: if self.model.manager.time >= self.nomin_fiat_order[ 0] + self.trade_duration: self.nomin_fiat_order[1].cancel() self.nomin_fiat_order = None if self.fiat_havven_order is not None: if self.model.manager.time >= self.fiat_havven_order[ 0] + self.trade_duration: self.fiat_havven_order[1].cancel() self.fiat_havven_order = None if self.available_nomins > 0: if len(self.model.datacollector.model_vars['0']) > 0: havven_supply = self.model.datacollector.model_vars[ 'Havven Supply'][-1] fiat_supply = self.model.datacollector.model_vars[ 'Fiat Supply'][-1] # buy into the market with more supply, as by virtue of there being more supply, # the market will probably have a better price... if havven_supply > fiat_supply: order = self.place_havven_nomin_bid_with_fee( self.available_nomins * self.sell_rate, self.havven_nomin_market.price * (Dec(1) - self.trade_premium)) if order is None: return self.nomin_havven_order = (self.model.manager.time, order) else: order = self.place_nomin_fiat_ask_with_fee( self.available_nomins * self.sell_rate, self.nomin_fiat_market.price * (Dec(1) + self.trade_premium)) if order is None: return self.nomin_fiat_order = (self.model.manager.time, order) if self.available_fiat > 0 and not self.fiat_havven_order: order = self.place_havven_fiat_bid_with_fee( hm.round_decimal(self.available_fiat * self.sell_rate), self.havven_fiat_market.price * (Dec(1) - self.trade_premium)) if order is None: return self.fiat_havven_order = (self.model.manager.time, order) if self.available_havvens > 0: self.escrow_havvens(self.available_havvens) issuable = self.max_issuance_rights() - self.issued_nomins if hm.round_decimal(issuable) > 0: self.issue_nomins(issuable)
def _fraction( self, qty: Dec, divisor: Dec = Dec(3), minimum: Dec = Dec(1)) -> Dec: """ Return a fraction of the given quantity, with a minimum. Used for depleting reserves gradually. """ return max(hm.round_decimal(qty / divisor), min(minimum, qty))
def _reverse_multiple_no_fees(self) -> Dec: """ The value multiple after one reverse arbitrage cycle, neglecting fees. """ # hav -> nom -> fiat -> hav return hm.round_decimal((self.havven_nomin_market.highest_bid_price() * self.nomin_fiat_market.highest_bid_price()) / \ self.havven_fiat_market.lowest_ask_price())
def _sell_quoted_with_fee(self, book: "ob.OrderBook", quantity: Dec) -> Optional["ob.Bid"]: """ Sell a quantity of the quoted currency into the given market, including the fee, as calculated by the provided function. """ price = book.lowest_ask_price() return book.buy( hm.round_decimal(book.quoted_qty_rcvd(quantity) / price), self)
def bids_not_lower_quoted_quantity(self, price: Dec, base_capital: Optional[Dec] = None ) -> Dec: """ Return the quantity of quoted currency you would obtain offering no less than a certain price, if you could spend up to a quantity of the base currency. """ bought = Dec(0) sold = Dec(0) for bid in self.bids_not_lower(price): if base_capital is not None and sold + bid.quantity > base_capital: bought += HavvenManager.round_decimal( (base_capital - sold) * bid.price) break sold += bid.quantity bought += HavvenManager.round_decimal(bid.price * bid.quantity) return bought
def asks_not_higher_base_quantity(self, price: Dec, quoted_capital: Optional[Dec] = None ) -> Dec: """ Return the quantity of base currency you would obtain offering no more than a certain price, if you could spend up to a quantity of the quoted currency. """ bought = Dec(0) sold = Dec(0) for ask in self.asks_not_higher(price): next_sold = HavvenManager.round_decimal(ask.price * ask.quantity) if quoted_capital is not None and sold + next_sold > quoted_capital: bought += HavvenManager.round_decimal( ask.quantity * (quoted_capital - sold) / next_sold) break sold += next_sold bought += ask.quantity return bought
def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.fiat_havven_order: Optional[Tuple[int, "ob.Bid"]] = None """The time the order was placed as well as the fiat/hvn order""" self.nomin_havven_order: Optional[Tuple[int, "ob.Bid"]] = None self.nomin_fiat_order: Optional[Tuple[int, "ob.Ask"]] = None self.sell_rate: Dec = hm.round_decimal(Dec(random.random() / 3 + 0.1)) self.trade_premium: Dec = Dec('0.01') self.trade_duration: int = 10 # step when initialised so nomins appear on the market. self.step()
def _sell_quoted(self, book: "ob.OrderBook", quantity: Dec) -> Optional["ob.Bid"]: """ Sell a quantity of the quoted currency into the given market. """ remaining_quoted = self.__getattribute__(f"available_{book.quoted}") quantity = min(quantity, remaining_quoted) if quantity < Dec( '0.0005' ): # TODO: remove workaround, and/or factor into epsilon variable return None next_qty = hm.round_decimal( min(quantity, book.lowest_ask_quantity()) / book.lowest_ask_price()) pre_sold = self.__getattribute__(f"available_{book.quoted}") bid = book.buy(next_qty, self) total_sold = pre_sold - self.__getattribute__( f"available_{book.quoted}") # Keep on bidding until we either run out of reserves or sellers, or we've bought enough. while bid is not None and not bid.active and total_sold < quantity and len( book.asks) == 0: next_qty = hm.round_decimal( min(quantity - total_sold, book.lowest_ask_quantity()) / book.lowest_ask_price()) pre_sold = self.__getattribute__(f"available_{book.quoted}") bid = book.buy(next_qty, self) total_sold += pre_sold - self.__getattribute__( f"available_{book.quoted}") if total_sold < quantity: if bid is not None: bid.cancel() price = book.lowest_ask_price() bid = book.bid(price, hm.round_decimal((quantity - total_sold) / price), self) return bid
def sell(self, quantity: Dec, agent: "ag.MarketPlayer") -> Optional[Ask]: """ Sell a quantity of the base currency at the best available price. """ price = HavvenManager.round_decimal( self.price_to_sell_quantity(quantity)) ask = self.ask(price, quantity, agent) # cancel the ask if it isn't filled immediately, as a market buy/sell should # always be filled (unless the market dries up) if not self.continuous_order_matching and ask: ask.cancel() return ask
def buy(self, quantity: Dec, agent: "ag.MarketPlayer") -> Optional[Bid]: """ Buy a quantity of the base currency at the best available price. """ price = HavvenManager.round_decimal( self.price_to_buy_quantity(quantity)) bid = self.bid(price, quantity, agent) # cancel the bid if it isn't filled immediately, as a market buy/sell should # always be filled (unless the market dries up) if not self.continuous_order_matching and bid: bid.cancel() return bid
def _issue_nomins_up_to(self, quantity: Dec) -> bool: """ If quantity > currently issued nomins, including fees to trade, issue more nomins If the player cant issue more nomins than the quantity, """ fee = HavvenManager.round_decimal(self.model.fee_manager.transferred_nomins_fee(quantity)) # if there are enough nomins, return if self.available_nomins > fee + quantity: return True nomins_needed = fee + quantity - self.available_nomins if self.remaining_issuance_rights() > nomins_needed: return self.issue_nomins(nomins_needed) else: return self.issue_nomins(self.remaining_issuance_rights())
def __init__(self, price: Dec, time: int, quantity: Dec, fee: Dec, issuer: "ag.MarketPlayer", book: "OrderBook") -> None: self.price = HavvenManager.round_decimal(price) """Quoted currency per unit of base currency.""" self.fee = fee """An extra fee charged on top of the face value of the order.""" self.time = time """The time this order was created, or last modified.""" self.quantity = quantity """Denominated in the base currency.""" self.issuer = issuer """The player which issued this order.""" self.book = book """The order book this order is listed on.""" self.active = self.quantity > 0 """Whether the order is actively listed or not."""
def __init__(self, model_settings: Dict[str, Any], fee_settings: Dict[str, Any], agent_settings: Dict[str, Any], havven_settings: Dict[str, Any]) -> None: """ :param model_settings: Setting that are modifiable on the frontend - agent_fraction: what percentage of each agent to use - num_agents: the total number of agents to use - utilisation_ratio_max: the max utilisation ratio for nomin issuance against havvens - continuous_order_matching: whether to match orders as they come, or at the end of each tick :param fee_settings: explained in feemanager.py :param agent_settings: explained in agentmanager.py :param havven_settings: explained in havvenmanager.py """ agent_fractions = model_settings['agent_fractions'] num_agents = model_settings['num_agents'] utilisation_ratio_max = model_settings['utilisation_ratio_max'] continuous_order_matching = model_settings['continuous_order_matching'] # Mesa setup. super().__init__() # The schedule will activate agents in a random order per step. self.schedule = RandomActivation(self) # Set up data collection. self.datacollector = stats.create_datacollector() # Initialise simulation managers. self.manager = HavvenManager(Dec(utilisation_ratio_max), continuous_order_matching, havven_settings) self.fee_manager = FeeManager(self.manager, fee_settings) self.market_manager = MarketManager(self.manager, self.fee_manager) self.mint = Mint(self.manager, self.market_manager) self.agent_manager = AgentManager(self, num_agents, agent_fractions, agent_settings)
def step(self) -> None: """ Find an exploitable arbitrage cycle. The only cycles that exist are HAV -> FIAT -> NOM -> HAV, its rotations, and the reverse cycles. This bot will consider those and act to exploit the most favourable such cycle if the profit available around that cycle is better than the profit threshold (including fees). """ self.havven_fiat_bid_qty = self.havven_fiat_market.highest_bid_quantity( ) self.havven_nomin_bid_qty = self.havven_nomin_market.highest_bid_quantity( ) self.nomin_fiat_bid_qty = self.nomin_fiat_market.highest_bid_quantity() self.nomin_fiat_ask_qty = hm.round_decimal( self.nomin_fiat_market.lowest_ask_quantity() * self.nomin_fiat_market.lowest_ask_price()) self.havven_nomin_ask_qty = hm.round_decimal( self.havven_nomin_market.lowest_ask_quantity() * self.havven_nomin_market.lowest_ask_price()) self.havven_fiat_ask_qty = hm.round_decimal( self.havven_fiat_market.lowest_ask_quantity() * self.havven_fiat_market.lowest_ask_price()) wealth = self.wealth() # Consider the forward direction cc_net_wealth = self.model.fiat_value( **self.forward_havven_cycle_balances()) - wealth nn_net_wealth = self.model.fiat_value( **self.forward_nomin_cycle_balances()) - wealth ff_net_wealth = self.model.fiat_value( **self.forward_fiat_cycle_balances()) - wealth max_net_wealth = max(cc_net_wealth, nn_net_wealth, ff_net_wealth) if max_net_wealth > self.profit_threshold: if cc_net_wealth == max_net_wealth: self.forward_havven_cycle_trade() elif nn_net_wealth == max_net_wealth: self.forward_nomin_cycle_trade() else: self.forward_fiat_cycle_trade() return # Now the reverse direction cc_net_wealth = self.model.fiat_value( **self.reverse_havven_cycle_balances()) - wealth nn_net_wealth = self.model.fiat_value( **self.reverse_nomin_cycle_balances()) - wealth ff_net_wealth = self.model.fiat_value( **self.reverse_fiat_cycle_balances()) - wealth max_net_wealth = max(cc_net_wealth, nn_net_wealth, ff_net_wealth) if max_net_wealth > self.profit_threshold: if cc_net_wealth == max_net_wealth: self.reverse_havven_cycle_trade() elif nn_net_wealth == max_net_wealth: self.reverse_nomin_cycle_trade() else: self.reverse_fiat_cycle_trade()
def _reverse_multiple(self) -> Dec: """The return after one reverse arbitrage cycle.""" # As above. If the fees were not just levied as percentages this would need to be updated. return hm.round_decimal(self._reverse_multiple_no_fees() / self._cycle_fee_rate())
def _forward_multiple(self) -> Dec: """The return after one forward arbitrage cycle.""" # Note, this only works because the fees are purely multiplicative. return hm.round_decimal(self._forward_multiple_no_fees() / self._cycle_fee_rate())
def seller_received_quantity(self, price: Dec, quantity: Dec) -> Dec: """ The quantity of the quoted currency received by a seller (fees deducted). """ return self.quoted_qty_rcvd( HavvenManager.round_decimal(price * quantity))
def _cycle_fee_rate(self) -> Dec: """Divide by this fee rate to determine losses after one traversal of an arbitrage cycle.""" return hm.round_decimal((Dec(1) + self.model.fee_manager.nomin_fee_rate) * \ (Dec(1) + self.model.fee_manager.havven_fee_rate) * \ (Dec(1) + self.model.fee_manager.fiat_fee_rate))
def _havven_nomin_ask(self) -> "ob.Ask": price = self.havven_nomin_market.price movement = hm.round_decimal( Dec(2 * random.random() - 1) * price * self.variance) return self.place_havven_nomin_ask( self._fraction(self.available_havvens, Dec(10)), price + movement)
def _nomin_fiat_bid(self) -> "ob.Bid": price = self.nomin_fiat_market.price movement = hm.round_decimal( Dec(2 * random.random() - 1) * price * self.variance) return self.place_nomin_fiat_bid( self._fraction(self.available_fiat, Dec(10)), price + movement)
def update_ask(self, ask: Ask, new_price: Dec, new_quantity: Dec, fee: Optional[Dec] = None) -> None: """ Update an Ask's details in the book, recomputing fees, cached quantities, and the user's unavailable currency totals. If fee is not None, then update the fee directly, rather than recomputing it. """ # Do nothing if the order is inactive. if not ask.active: return new_price = HavvenManager.round_decimal(new_price) new_quantity = HavvenManager.round_decimal(new_quantity) if fee is not None: fee = HavvenManager.round_decimal(fee) # Do nothing if the price and quantity would remain unchanged. if ask.price == new_price and ask.quantity == new_quantity: if fee is None or fee == ask.fee: return else: print(ask) raise Exception( "Fee changed, but price and quantity are unchanged...") # If the ask is updated with a non-positive quantity, it is cancelled. if new_quantity <= 0: self.cancel_ask(ask) return # Compute the new fee new_fee = fee if fee is None: new_fee = self.seller_fee(new_price, new_quantity) # Update the unavailable quantities for this ask, # deducting the old and crediting the new. ask.issuer.__dict__[f"unavailable_{self.base}"] += \ (new_quantity + new_fee) - (ask.quantity + ask.fee) if ask.price == new_price: # We may assume the current price is already recorded, # so no need to call _ask_bucket_add_ which checks before # inserting. Something is wrong if the key is not found. self.ask_price_buckets[new_price] += (new_quantity - ask.quantity) # As the price is unchanged, order book position need not be # updated, just set the quantity and fee. ask.quantity = new_quantity ask.fee = new_fee else: # Deduct the old quantity from its price bucket, # and add the new quantity into the appropriate bucket. self._ask_bucket_deduct(ask.price, ask.quantity) self._ask_bucket_add(new_price, new_quantity) # Since the price changed, update the ask's position # in the book. self.asks.remove(ask) ask.price = new_price ask.quantity = new_quantity ask.fee = new_fee # Only set the timestep if the price was updated. ask.time = self.time self.asks.add(ask) # Advance time. self.step()
def setup(self, init_value: Dec): endowment = hm.round_decimal(init_value * Dec(4)) self.fiat = init_value self.model.endow_havvens(self, endowment)
def buyer_fee(self, price: Dec, quantity: Dec) -> Dec: """ Return the fee paid on the quoted end (by the buyer) for a bid of the given quantity and price. """ return self.quoted_fee(HavvenManager.round_decimal(price * quantity))