diff --git a/simulation/agents/arbitrageur.py b/simulation/agents/arbitrageur.py index 446185c..c1c1f93 100644 --- a/simulation/agents/arbitrageur.py +++ b/simulation/agents/arbitrageur.py @@ -1,94 +1,371 @@ +from typing import Dict from decimal import Decimal as Dec from .marketplayer import MarketPlayer from managers import HavvenManager as hm +from orderbook import OrderBook class Arbitrageur(MarketPlayer): """Wants to find arbitrage cycles and exploit them to equalise prices.""" + def __init__(self, unique_id: int, havven: "model.Havven", + fiat: Dec = Dec(0), curits: Dec = Dec(0), + nomins: Dec = Dec(0), + profit_threshold: Dec = Dec('0.05')) -> None: + + super().__init__(unique_id, havven, fiat=fiat, + curits=curits, nomins=nomins) + + self.profit_threshold = profit_threshold + """ + This arbitrageur will only trade if it can make a profit higher than + this fraction. + """ + + # Cached values of the amount available in arb cycles for performance + # and neatness. + self.curit_fiat_bid_qty = Dec(0) + self.curit_nomin_bid_qty = Dec(0) + self.nomin_fiat_bid_qty = Dec(0) + self.nomin_fiat_ask_qty = Dec(0) + self.curit_nomin_ask_qty = Dec(0) + self.curit_fiat_ask_qty = Dec(0) def step(self) -> None: - """Find an exploitable arbitrage cycle.""" - # The only cycles that exist are CUR -> FIAT -> NOM -> CUR, - # its rotations, and the reverse cycles. - # The bot will act to place orders in all markets at once, - # if there is an arbitrage opportunity, taking into account - # the fee rates. - - if self._forward_multiple_() <= 1 and self._reverse_multiple_() <= 1: - return - - if self._forward_multiple_() > 1: - # Trade in the forward direction - # TODO: work out which rotation of this cycle would be the least wasteful - # cur -> fiat -> nom -> cur - fn_price = Dec('1.0') / self.model.market_manager.nomin_fiat_market.lowest_ask_price() - nc_price = Dec('1.0') / self.model.market_manager.curit_nomin_market.lowest_ask_price() - - cf_qty = Dec(sum(b.quantity for b in self.model.market_manager.curit_fiat_market.highest_bids())) - fn_qty = Dec(sum(a.quantity for a in self.model.market_manager.nomin_fiat_market.lowest_asks())) - nc_qty = Dec(sum(a.quantity for a in self.model.market_manager.curit_nomin_market.lowest_asks())) - - # cur_val = self.model.fiat_value(curits=self.available_curits) - # nom_val = self.model.fiat_value(nomins=self.available_nomins) - - # if cur_val < nom_val and cur_val < self.available_fiat: - """ - c_qty = min(self.available_curits, cf_qty) - self.sell_curits_for_fiat(c_qty) - - f_qty = min(self.available_fiat, fn_qty * fn_price) - self.sell_fiat_for_curits(f_qty) - - n_qty = min(self.available_nomins, nc_qty * nc_price) - self.sell_nomins_for_curits(n_qty) - """ - c_qty = min(self.available_curits, cf_qty) - self.sell_curits_for_fiat(c_qty) - - f_qty = min(self.available_fiat, hm.round_decimal(fn_qty * fn_price)) - self.sell_fiat_for_curits(f_qty) - - n_qty = min(self.available_nomins, hm.round_decimal(nc_qty * nc_price)) - self.sell_nomins_for_curits(n_qty) - - """ - elif nom_val < cur_val and nom_val < self.available_fiat: - n_qty = min(self.available_nomins, nc_qty) - self.sell_nomins_for_curits(n_qty) - - c_qty = min(self.available_curits, n_qty * nc_price) - self.sell_curits_for_fiat(c_qty) - - f_qty = min(self.available_fiat, fn_qty * fn_price) - self.sell_fiat_for_curits(f_qty) - - n_qty = min(self.available_nomins, nc_qty * nc_price) - self.sell_nomins_for_curits(n_qty) + """ + Find an exploitable arbitrage cycle. + + The only cycles that exist are CUR -> FIAT -> NOM -> CUR, + 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.curit_fiat_bid_qty = self.curit_fiat_market.highest_bid_quantity() + self.curit_nomin_bid_qty = self.curit_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.curit_nomin_ask_qty = hm.round_decimal(self.curit_nomin_market.lowest_ask_quantity() + * self.curit_nomin_market.lowest_ask_price()) + self.curit_fiat_ask_qty = hm.round_decimal(self.curit_fiat_market.lowest_ask_quantity() + * self.curit_fiat_market.lowest_ask_price()) + + wealth = self.wealth() + + # Consider the forward direction + cc_net_wealth = self.model.fiat_value(**self.forward_curit_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_curit_cycle_trade() + elif nn_net_wealth == max_net_wealth: + self.forward_nomin_cycle_trade() else: - """ + self.forward_fiat_cycle_trade() - elif self._reverse_multiple_() > 1: - # Trade in the reverse direction - # cur -> nom -> fiat -> cur - fc_price = hm.round_decimal(Dec('1.0') / self.model.market_manager.curit_fiat_market.lowest_ask_price()) + # Now the reverse direction + cc_net_wealth = self.model.fiat_value(**self.reverse_curit_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) - # These seemingly-redundant Dec constructors are necessary because if these lists are empty, - # the sum returns 0 as an integer. - cn_qty = Dec(sum(b.quantity for b in self.model.market_manager.curit_nomin_market.highest_bids())) - nf_qty = Dec(sum(b.quantity for b in self.model.market_manager.nomin_fiat_market.highest_bids())) - fc_qty = Dec(sum(a.quantity for a in self.model.market_manager.curit_fiat_market.lowest_asks())) + if max_net_wealth > self.profit_threshold: + if cc_net_wealth == max_net_wealth: + self.reverse_curit_cycle_trade() + elif nn_net_wealth == max_net_wealth: + self.reverse_nomin_cycle_trade() + else: + self.reverse_fiat_cycle_trade() - c_qty = min(self.available_curits, cn_qty) - self.sell_curits_for_nomins(c_qty) + @staticmethod + def _base_to_quoted_yield_(market: OrderBook, base_capital: Dec) -> Dec: + """ + The quantity of the quoted currency you could obtain at the current + best market price, selling a given quantity of the base currency. + """ + price = market.highest_bid_price() + feeless_capital = market._ask_qty_received_fn_(base_capital) + return market.bids_not_lower_quoted_quantity(price, feeless_capital) + + @staticmethod + def _quoted_to_base_yield(market: OrderBook, quoted_capital: Dec) -> Dec: + """ + The quantity of the base currency you could obtain at the current + best market price, selling a given quantity of the quoted currency. + """ + price = market.lowest_ask_price() + feeless_capital = market._bid_qty_received_fn_(quoted_capital) + return market.asks_not_higher_base_quantity(price, feeless_capital) - n_qty = min(self.available_nomins, nf_qty) - self.sell_nomins_for_fiat(n_qty) + def curit_to_fiat_yield(self, capital: Dec) -> Dec: + """ + The quantity of fiat obtained, spending a quantity of curits. + """ + return self._base_to_quoted_yield_(self.curit_fiat_market, capital) - f_qty = min(self.available_fiat, hm.round_decimal(fc_qty * fc_price)) - self.sell_nomins_for_curits(n_qty) + def fiat_to_curit_yield(self, capital: Dec) -> Dec: + """ + The quantity of curits obtained, spending a quantity of fiat. + """ + return self._quoted_to_base_yield(self.curit_fiat_market, capital) + + def curit_to_nomin_yield(self, capital: Dec) -> Dec: + """ + The quantity of nomins obtained, spending a quantity of curits. + """ + return self._base_to_quoted_yield_(self.curit_nomin_market, capital) + + def nomin_to_curit_yield(self, capital: Dec) -> Dec: + """ + The quantity of curits obtained, spending a quantity of nomins. + """ + return self._quoted_to_base_yield(self.curit_nomin_market, capital) + + def nomin_to_fiat_yield(self, capital: Dec) -> Dec: + """ + The quantity of fiat obtained, spending a quantity of nomins. + """ + return self._base_to_quoted_yield_(self.nomin_fiat_market, capital) + + def fiat_to_nomin_yield(self, capital: Dec) -> Dec: + """ + The quantity of nomins obtained, spending a quantity of fiat. + """ + return self._quoted_to_base_yield(self.nomin_fiat_market, capital) + + def forward_curit_cycle_balances(self) -> Dict[str, Dec]: + """ + Return the estimated wallet balances of this agent after + one forward curit arbitrage cycle: curits -> fiat -> nomins -> curits. + """ + curits = self.curits + nomins = self.nomins + fiat = self.fiat + + c_qty = min(curits, self.curit_fiat_bid_qty) + curits -= c_qty + f_qty = self.curit_to_fiat_yield(c_qty) + fiat += f_qty + f_qty = min(f_qty, self.nomin_fiat_ask_qty) + fiat -= f_qty + n_qty = self.fiat_to_nomin_yield(f_qty) + nomins += n_qty + n_qty = min(n_qty, self.curit_nomin_ask_qty) + nomins -= n_qty + c_qty = self.nomin_to_curit_yield(n_qty) + curits += c_qty + + return {"curits": curits, "nomins": nomins, "fiat": fiat} + + def forward_curit_cycle_trade(self) -> None: + """ + Perform a single forward curit arbitrage cycle; + curits -> fiat -> nomins -> curits. + """ + c_qty = min(self.available_curits, self.curit_fiat_bid_qty) + pre_fiat = self.fiat + self.sell_curits_for_fiat_with_fee(c_qty) + f_qty = min(self.fiat - pre_fiat, self.nomin_fiat_ask_qty) + pre_nomins = self.nomins + self.sell_fiat_for_nomins_with_fee(f_qty) + n_qty = min(self.nomins - pre_nomins, self.curit_nomin_ask_qty) + self.sell_nomins_for_curits_with_fee(n_qty) + + def forward_fiat_cycle_balances(self) -> Dict[str, Dec]: + """ + Return the estimated wallet balances of this agent after + one forward fiat arbitrage cycle: fiat -> nomins -> curits -> fiat. + """ + curits = self.curits + nomins = self.nomins + fiat = self.fiat + + f_qty = min(fiat, self.nomin_fiat_ask_qty) + fiat -= f_qty + n_qty = self.fiat_to_nomin_yield(f_qty) + nomins += n_qty + n_qty = min(n_qty, self.curit_nomin_ask_qty) + nomins -= n_qty + c_qty = self.nomin_to_curit_yield(n_qty) + curits += c_qty + c_qty = min(c_qty, self.curit_fiat_bid_qty) + curits -= c_qty + f_qty = self.curit_to_fiat_yield(c_qty) + fiat += f_qty + + return {"curits": curits, "nomins": nomins, "fiat": fiat} + + def forward_fiat_cycle_trade(self) -> None: + """ + Perform a single forward fiat arbitrage cycle; + fiat -> nomins -> curits -> fiat. + """ + f_qty = min(self.available_fiat, self.nomin_fiat_ask_qty) + pre_nomins = self.nomins + self.sell_fiat_for_nomins_with_fee(f_qty) + n_qty = min(self.nomins - pre_nomins, self.curit_nomin_ask_qty) + pre_curits = self.curits + self.sell_nomins_for_curits_with_fee(n_qty) + c_qty = min(self.curits - pre_curits, self.curit_fiat_bid_qty) + self.sell_curits_for_fiat_with_fee(c_qty) + + def forward_nomin_cycle_balances(self) -> Dict[str, Dec]: + """ + Return the estimated wallet balances of this agent after + one forward nomin arbitrage cycle: nomins -> curits -> fiat -> nomins. + """ + curits = self.curits + nomins = self.nomins + fiat = self.fiat + + n_qty = min(nomins, self.curit_nomin_ask_qty) + nomins -= n_qty + c_qty = self.nomin_to_curit_yield(n_qty) + curits += c_qty + c_qty = min(c_qty, self.curit_fiat_bid_qty) + curits -= c_qty + f_qty = self.curit_to_fiat_yield(c_qty) + fiat += f_qty + f_qty = min(f_qty, self.nomin_fiat_ask_qty) + fiat -= f_qty + n_qty = self.fiat_to_nomin_yield(f_qty) + nomins += n_qty + + return {"curits": curits, "nomins": nomins, "fiat": fiat} + + def forward_nomin_cycle_trade(self) -> None: + """ + Perform a single forward nomin arbitrage cycle; + nomins -> curits -> fiat -> nomins. + """ + n_qty = min(self.available_nomins, self.curit_nomin_ask_qty) + pre_curits = self.curits + self.sell_nomins_for_curits_with_fee(n_qty) + c_qty = min(self.curits - pre_curits, self.curit_fiat_bid_qty) + pre_fiat = self.fiat + self.sell_curits_for_fiat_with_fee(c_qty) + f_qty = min(self.fiat - pre_fiat, self.nomin_fiat_ask_qty) + self.sell_fiat_for_nomins_with_fee(f_qty) + + def reverse_curit_cycle_balances(self) -> Dict[str, Dec]: + """ + Return the estimated wallet balances of this agent after + one reverse curit arbitrage cycle: curits -> nomins -> fiat -> curits. + """ + curits = self.curits + nomins = self.nomins + fiat = self.fiat + + c_qty = min(curits, self.curit_nomin_bid_qty) + curits -= c_qty + n_qty = self.curit_to_nomin_yield(c_qty) + nomins += n_qty + n_qty = min(n_qty, self.nomin_fiat_bid_qty) + nomins -= n_qty + f_qty = self.nomin_to_fiat_yield(n_qty) + fiat += f_qty + f_qty = min(f_qty, self.curit_fiat_ask_qty) + fiat -= f_qty + c_qty = self.fiat_to_curit_yield(f_qty) + curits += c_qty + + return {"curits": curits, "nomins": nomins, "fiat": fiat} + + def reverse_curit_cycle_trade(self) -> None: + """ + Perform a single reverse curit arbitrage cycle; + curits -> nomins -> fiat -> curits. + """ + c_qty = min(self.available_curits, self.curit_nomin_bid_qty) + pre_nomins = self.nomins + self.sell_curits_for_nomins_with_fee(c_qty) + n_qty = min(self.nomins - pre_nomins, self.nomin_fiat_bid_qty) + pre_fiat = self.fiat + self.sell_nomins_for_fiat_with_fee(n_qty) + f_qty = min(self.fiat - pre_fiat, self.curit_fiat_ask_qty) + self.sell_fiat_for_curits_with_fee(f_qty) + + def reverse_nomin_cycle_balances(self) -> Dict[str, Dec]: + """ + Return the estimated wallet balances of this agent after + one reverse nomin arbitrage cycle: nomins -> fiat -> curits -> nomins. + """ + curits = self.curits + nomins = self.nomins + fiat = self.fiat + + n_qty = min(nomins, self.nomin_fiat_bid_qty) + nomins -= n_qty + f_qty = self.nomin_to_fiat_yield(n_qty) + fiat += f_qty + f_qty = min(f_qty, self.curit_fiat_ask_qty) + fiat -= f_qty + c_qty = self.fiat_to_curit_yield(f_qty) + curits += c_qty + c_qty = min(c_qty, self.curit_nomin_bid_qty) + curits -= c_qty + n_qty = self.curit_to_nomin_yield(c_qty) + nomins += n_qty + + return {"curits": curits, "nomins": nomins, "fiat": fiat} + + def reverse_nomin_cycle_trade(self) -> None: + """ + Perform a single reverse nomin arbitrage cycle; + nomins -> fiat -> curits -> nomins. + """ + n_qty = min(self.available_nomins, self.nomin_fiat_bid_qty) + pre_fiat = self.fiat + self.sell_nomins_for_fiat_with_fee(n_qty) + f_qty = min(self.fiat - pre_fiat, self.curit_fiat_ask_qty) + pre_curits = self.curits + self.sell_fiat_for_curits_with_fee(f_qty) + c_qty = min(self.curits - pre_curits, self.curit_nomin_bid_qty) + self.sell_curits_for_nomins_with_fee(c_qty) + + def reverse_fiat_cycle_balances(self) -> Dict[str, Dec]: + """ + Return the estimated wallet balances of this agent after + one reverse fiat arbitrage cycle: fiat -> curits -> nomins -> fiat. + """ + curits = self.curits + nomins = self.nomins + fiat = self.fiat + + f_qty = min(fiat, self.curit_fiat_ask_qty) + fiat -= f_qty + c_qty = self.fiat_to_curit_yield(f_qty) + curits += c_qty + c_qty = min(c_qty, self.curit_nomin_bid_qty) + curits -= c_qty + n_qty = self.curit_to_nomin_yield(c_qty) + nomins += n_qty + n_qty = min(n_qty, self.nomin_fiat_bid_qty) + nomins -= n_qty + f_qty = self.nomin_to_fiat_yield(n_qty) + fiat += f_qty + + return {"curits": curits, "nomins": nomins, "fiat": fiat} + + def reverse_fiat_cycle_trade(self) -> None: + """ + Perform a single reverse fiat arbitrage cycle; + fiat -> curits -> nomins -> fiat. + """ + f_qty = min(self.available_fiat, self.curit_fiat_ask_qty) + pre_curits = self.curits + self.sell_fiat_for_curits_with_fee(f_qty) + c_qty = min(self.curits - pre_curits, self.curit_nomin_bid_qty) + self.sell_curits_for_nomins_with_fee(c_qty) + pre_nomins = self.nomins + n_qty = min(self.nomins - pre_nomins, self.nomin_fiat_bid_qty) + self.sell_nomins_for_fiat_with_fee(n_qty) def _cycle_fee_rate_(self) -> Dec: """Divide by this fee rate to determine losses after one traversal of an arbitrage cycle.""" @@ -101,18 +378,18 @@ def _forward_multiple_no_fees_(self) -> Dec: The value multiple after one forward arbitrage cycle, neglecting fees. """ # cur -> fiat -> nom -> cur - return hm.round_decimal(self.model.market_manager.curit_fiat_market.highest_bid_price() / \ - (self.model.market_manager.nomin_fiat_market.lowest_ask_price() * - self.model.market_manager.curit_nomin_market.lowest_ask_price())) + return hm.round_decimal(self.curit_fiat_market.highest_bid_price() / \ + (self.nomin_fiat_market.lowest_ask_price() * + self.curit_nomin_market.lowest_ask_price())) def _reverse_multiple_no_fees_(self) -> Dec: """ The value multiple after one reverse arbitrage cycle, neglecting fees. """ # cur -> nom -> fiat -> cur - return hm.round_decimal((self.model.market_manager.curit_nomin_market.highest_bid_price() * - self.model.market_manager.nomin_fiat_market.highest_bid_price()) / \ - self.model.market_manager.curit_fiat_market.lowest_ask_price()) + return hm.round_decimal((self.curit_nomin_market.highest_bid_price() * + self.nomin_fiat_market.highest_bid_price()) / \ + self.curit_fiat_market.lowest_ask_price()) def _forward_multiple_(self) -> Dec: """The return after one forward arbitrage cycle.""" diff --git a/simulation/agents/banker.py b/simulation/agents/banker.py index 5302ac5..b4b4afe 100644 --- a/simulation/agents/banker.py +++ b/simulation/agents/banker.py @@ -21,13 +21,13 @@ def step(self) -> None: if self.fiat_curit_order: self.fiat_curit_order.cancel() fiat = self.model.fee_manager.transferred_fiat_received(self.available_fiat) - self.fiat_curit_order = self.sell_fiat_for_curits(hm.round_decimal(fiat * self.rate)) + self.fiat_curit_order = self.sell_fiat_for_curits_with_fee(hm.round_decimal(fiat * self.rate)) if hm.round_decimal(self.available_nomins) > 0: if self.nomin_curit_order: self.nomin_curit_order.cancel() nomins = self.model.fee_manager.transferred_nomins_received(self.available_nomins) - self.nomin_curit_order = self.sell_nomins_for_curits(nomins) + self.nomin_curit_order = self.sell_nomins_for_curits_with_fee(nomins) if hm.round_decimal(self.available_curits) > 0: self.escrow_curits(self.available_curits) diff --git a/simulation/agents/centralbank.py b/simulation/agents/centralbank.py index 1dbe28f..c6205f7 100644 --- a/simulation/agents/centralbank.py +++ b/simulation/agents/centralbank.py @@ -42,7 +42,7 @@ def step(self) -> None: self.cancel_orders() if self.curit_target is not None: - curit_price = self.model.market_manager.curit_fiat_market.price + curit_price = self.curit_fiat_market.price # Price is too high, it should decrease: we will sell curits at a discount. if curit_price > hm.round_decimal(self.curit_target * (Dec(1) + self.tolerance)): # If we have curits, sell them. @@ -84,7 +84,7 @@ def step(self) -> None: self.issue_nomins(issuance_rights) if self.nomin_target is not None: - nomin_price = self.model.market_manager.nomin_fiat_market.price + nomin_price = self.nomin_fiat_market.price # Price is too high, it should decrease: we will sell nomins at a discount. if nomin_price > hm.round_decimal(self.nomin_target * (Dec(1) + self.tolerance)): if self.available_nomins > 0: diff --git a/simulation/agents/marketplayer.py b/simulation/agents/marketplayer.py index 94ab590..fab8f8f 100644 --- a/simulation/agents/marketplayer.py +++ b/simulation/agents/marketplayer.py @@ -1,4 +1,4 @@ -from typing import Set, Tuple, Callable +from typing import Set, List, Tuple, Callable, Optional from collections import namedtuple from decimal import Decimal as Dec @@ -39,10 +39,26 @@ def __init__(self, unique_id: int, havven: "model.Havven", self.initial_wealth: Dec = self.wealth() self.orders: Set["ob.LimitOrder"] = set() + self.trades: List["ob.TradeRecord"] = [] def __str__(self) -> str: return self.name + @property + def curit_fiat_market(self) -> "ob.OrderBook": + """The curit-fiat market this player trades on.""" + return self.model.market_manager.curit_fiat_market + + @property + def nomin_fiat_market(self) -> "ob.OrderBook": + """The nomin-fiat market this player trades on.""" + return self.model.market_manager.nomin_fiat_market + + @property + def curit_nomin_market(self) -> "ob.OrderBook": + """The curit-nomin market this player trades on.""" + return self.model.market_manager.curit_nomin_market + @property def name(self) -> str: """ @@ -201,12 +217,34 @@ def burn_nomins(self, value: Dec) -> bool: return self.model.mint.burn_nomins(self, value) def _sell_quoted_(self, book: "ob.OrderBook", quantity: Dec, - premium: Dec = Dec('0')) -> "ob.Bid": + premium: Dec = Dec('0')) -> Optional["ob.Bid"]: """ Sell a quantity of the quoted currency into the given market. """ - price = book.lowest_ask_price() - return book.buy(hm.round_decimal(quantity/price), self, premium) + + remaining_quoted = self.__getattribute__(f"available_{book.quoted}") + quantity = min(quantity, remaining_quoted) + if quantity == 0: + 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, premium) + total_sold = pre_sold - self.__getattribute__(f"available_{book.quoted}") + + while bid is not None and not bid.active and total_sold < quantity: + 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, premium) + 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_base_(self, book: "ob.OrderBook", quantity: Dec, discount: Dec = Dec('0')) -> "ob.Ask": @@ -220,48 +258,42 @@ def sell_nomins_for_curits(self, quantity: Dec, """ Sell a quantity of nomins to buy curits. """ - return self._sell_quoted_(self.model.market_manager.curit_nomin_market, - quantity, premium) + return self._sell_quoted_(self.curit_nomin_market, quantity, premium) def sell_curits_for_nomins(self, quantity: Dec, discount: Dec = Dec('0')) -> "ob.Ask": """ Sell a quantity of curits to buy nomins. """ - return self._sell_base_(self.model.market_manager.curit_nomin_market, - quantity, discount) + return self._sell_base_(self.curit_nomin_market, quantity, discount) def sell_fiat_for_curits(self, quantity: Dec, premium: Dec = Dec('0')) -> "ob.Bid": """ Sell a quantity of fiat to buy curits. """ - return self._sell_quoted_(self.model.market_manager.curit_fiat_market, - quantity, premium) + return self._sell_quoted_(self.curit_fiat_market, quantity, premium) def sell_curits_for_fiat(self, quantity: Dec, discount: Dec = Dec('0')) -> "ob.Ask": """ Sell a quantity of curits to buy fiat. """ - return self._sell_base_(self.model.market_manager.curit_fiat_market, - quantity, discount) + return self._sell_base_(self.curit_fiat_market, quantity, discount) def sell_fiat_for_nomins(self, quantity: Dec, premium: Dec = Dec('0')) -> "ob.Bid": """ Sell a quantity of fiat to buy nomins. """ - return self._sell_quoted_(self.model.market_manager.nomin_fiat_market, - quantity, premium) + return self._sell_quoted_(self.nomin_fiat_market, quantity, premium) def sell_nomins_for_fiat(self, quantity: Dec, discount: Dec = Dec('0')) -> "ob.Ask": """ Sell a quantity of nomins to buy fiat. """ - return self._sell_base_(self.model.market_manager.nomin_fiat_market, - quantity, discount) + return self._sell_base_(self.nomin_fiat_market, quantity, discount) def _sell_quoted_with_fee_(self, received_qty_fn: Callable[[Dec], Dec], book: "ob.OrderBook", quantity: Dec, @@ -270,8 +302,7 @@ def _sell_quoted_with_fee_(self, received_qty_fn: Callable[[Dec], Dec], 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(received_qty_fn(hm.round_decimal(quantity/price)), self, premium) + return self._sell_quoted_(book, received_qty_fn(quantity), premium) def _sell_base_with_fee_(self, received_qty_fn: Callable[[Dec], Dec], book: "ob.OrderBook", quantity: Dec, @@ -280,7 +311,7 @@ def _sell_base_with_fee_(self, received_qty_fn: Callable[[Dec], Dec], Sell a quantity of the base currency into the given market, including the fee, as calculated by the provided function. """ - return book.sell(received_qty_fn(quantity), self, discount) + return self._sell_base_(book, received_qty_fn(quantity), discount) def sell_nomins_for_curits_with_fee(self, quantity: Dec, premium: Dec = Dec('0')) -> "ob.Bid": @@ -288,8 +319,7 @@ def sell_nomins_for_curits_with_fee(self, quantity: Dec, Sell a quantity of nomins (including fee) to buy curits. """ return self._sell_quoted_with_fee_(self.model.fee_manager.transferred_nomins_received, - self.model.market_manager.curit_nomin_market, - quantity, premium) + self.curit_nomin_market, quantity, premium) def sell_curits_for_nomins_with_fee(self, quantity: Dec, discount: Dec = Dec('0')) -> "ob.Ask": @@ -297,8 +327,7 @@ def sell_curits_for_nomins_with_fee(self, quantity: Dec, Sell a quantity of curits (including fee) to buy nomins. """ return self._sell_base_with_fee_(self.model.fee_manager.transferred_curits_received, - self.model.market_manager.curit_nomin_market, - quantity, discount) + self.curit_nomin_market, quantity, discount) def sell_fiat_for_curits_with_fee(self, quantity: Dec, premium: Dec = Dec('0')) -> "ob.Bid": @@ -306,8 +335,7 @@ def sell_fiat_for_curits_with_fee(self, quantity: Dec, Sell a quantity of fiat (including fee) to buy curits. """ return self._sell_quoted_with_fee_(self.model.fee_manager.transferred_fiat_received, - self.model.market_manager.curit_fiat_market, - quantity, premium) + self.curit_fiat_market, quantity, premium) def sell_curits_for_fiat_with_fee(self, quantity: Dec, discount: Dec = Dec('0')) -> "ob.Ask": @@ -315,8 +343,7 @@ def sell_curits_for_fiat_with_fee(self, quantity: Dec, Sell a quantity of curits (including fee) to buy fiat. """ return self._sell_base_with_fee_(self.model.fee_manager.transferred_curits_received, - self.model.market_manager.curit_fiat_market, - quantity, discount) + self.curit_fiat_market, quantity, discount) def sell_fiat_for_nomins_with_fee(self, quantity: Dec, premium: Dec = Dec('0')) -> "ob.Bid": @@ -324,8 +351,7 @@ def sell_fiat_for_nomins_with_fee(self, quantity: Dec, Sell a quantity of fiat (including fee) to buy nomins. """ return self._sell_quoted_with_fee_(self.model.fee_manager.transferred_fiat_received, - self.model.market_manager.nomin_fiat_market, - quantity, premium) + self.nomin_fiat_market, quantity, premium) def sell_nomins_for_fiat_with_fee(self, quantity: Dec, discount: Dec = Dec('0')) -> "ob.Ask": @@ -333,44 +359,43 @@ def sell_nomins_for_fiat_with_fee(self, quantity: Dec, Sell a quantity of nomins (including fee) to buy fiat. """ return self._sell_base_with_fee_(self.model.fee_manager.transferred_nomins_received, - self.model.market_manager.nomin_fiat_market, - quantity, discount) + self.nomin_fiat_market, quantity, discount) def place_curit_fiat_bid(self, quantity: Dec, price: Dec) -> "ob.Bid": """ Place a bid for a quantity of curits, at a price in fiat. """ - return self.model.market_manager.curit_fiat_market.bid(price, quantity, self) + return self.curit_fiat_market.bid(price, quantity, self) def place_curit_fiat_ask(self, quantity: Dec, price: Dec) -> "ob.Ask": """ Place an ask for fiat with a quantity of curits, at a price in fiat. """ - return self.model.market_manager.curit_fiat_market.ask(price, quantity, self) + return self.curit_fiat_market.ask(price, quantity, self) def place_nomin_fiat_bid(self, quantity: Dec, price: Dec) -> "ob.Bid": """ Place a bid for a quantity of nomins, at a price in fiat. """ - return self.model.market_manager.nomin_fiat_market.bid(price, quantity, self) + return self.nomin_fiat_market.bid(price, quantity, self) def place_nomin_fiat_ask(self, quantity: Dec, price: Dec) -> "ob.Ask": """ Place an ask for fiat with a quantity of nomins, at a price in fiat. """ - return self.model.market_manager.nomin_fiat_market.ask(price, quantity, self) + return self.nomin_fiat_market.ask(price, quantity, self) def place_curit_nomin_bid(self, quantity: Dec, price: Dec) -> "ob.Bid": """ Place a bid for a quantity of curits, at a price in nomins. """ - return self.model.market_manager.curit_nomin_market.bid(price, quantity, self) + return self.curit_nomin_market.bid(price, quantity, self) def place_curit_nomin_ask(self, quantity: Dec, price: Dec) -> "ob.Ask": """ Place an ask for nomins with a quantity of curits, at a price in nomins. """ - return self.model.market_manager.curit_nomin_market.ask(price, quantity, self) + return self.curit_nomin_market.ask(price, quantity, self) def place_curit_fiat_bid_with_fee(self, quantity: Dec, price: Dec) -> "ob.Bid": """ @@ -379,14 +404,14 @@ def place_curit_fiat_bid_with_fee(self, quantity: Dec, price: Dec) -> "ob.Bid": # Note, only works because the fee is multiplicative, we're calculating the fee not # on the quantity we are actually transferring, which is (quantity*price) qty = self.model.fee_manager.transferred_fiat_received(quantity) - return self.model.market_manager.curit_fiat_market.bid(price, qty, self) + return self.curit_fiat_market.bid(price, qty, self) def place_curit_fiat_ask_with_fee(self, quantity: Dec, price: Dec) -> "ob.Ask": """ Place an ask for fiat with a quantity of curits, including the fee, at a price in fiat. """ qty = self.model.fee_manager.transferred_curits_received(quantity) - return self.model.market_manager.curit_fiat_market.ask(price, qty, self) + return self.curit_fiat_market.ask(price, qty, self) def place_nomin_fiat_bid_with_fee(self, quantity: Dec, price: Dec) -> "ob.Bid": """ @@ -395,14 +420,14 @@ def place_nomin_fiat_bid_with_fee(self, quantity: Dec, price: Dec) -> "ob.Bid": # Note, only works because the fee is multiplicative, we're calculating the fee not # on the quantity we are actually transferring, which is (quantity*price) qty = self.model.fee_manager.transferred_fiat_received(quantity) - return self.model.market_manager.nomin_fiat_market.bid(price, qty, self) + return self.nomin_fiat_market.bid(price, qty, self) def place_nomin_fiat_ask_with_fee(self, quantity: Dec, price: Dec) -> "ob.Ask": """ Place an ask for fiat with a quantity of nomins, including the fee, at a price in fiat. """ qty = self.model.fee_manager.transferred_nomins_received(quantity) - return self.model.market_manager.nomin_fiat_market.ask(price, qty, self) + return self.nomin_fiat_market.ask(price, qty, self) def place_curit_nomin_bid_with_fee(self, quantity: Dec, price: Dec) -> "ob.Bid": """ @@ -411,14 +436,14 @@ def place_curit_nomin_bid_with_fee(self, quantity: Dec, price: Dec) -> "ob.Bid": # Note, only works because the fee is multiplicative, we're calculating the fee not # on the quantity we are actually transferring, which is (quantity*price) qty = self.model.fee_manager.transferred_nomins_received(quantity) - return self.model.market_manager.curit_nomin_market.bid(price, qty, self) + return self.curit_nomin_market.bid(price, qty, self) def place_curit_nomin_ask_with_fee(self, quantity: Dec, price: Dec) -> "ob.Ask": """ Place an ask for nomins with a quantity of curits, including the fee, at a price in nomins. """ qty = self.model.fee_manager.transferred_curits_received(quantity) - return self.model.market_manager.curit_nomin_market.ask(price, qty, self) + return self.curit_nomin_market.ask(price, qty, self) @property def available_fiat(self) -> Dec: @@ -447,11 +472,11 @@ def notify_cancelled(self, order: "ob.LimitOrder") -> None: """ pass - def notify_filled(self, order: "ob.LimitOrder") -> None: + def notify_trade(self, record: "ob.TradeRecord") -> None: """ Notify this agent that its order was filled. """ - pass + self.trades.append(record) def step(self) -> None: pass diff --git a/simulation/agents/nomin_shorter.py b/simulation/agents/nomin_shorter.py index dcff962..00e31b0 100644 --- a/simulation/agents/nomin_shorter.py +++ b/simulation/agents/nomin_shorter.py @@ -56,7 +56,7 @@ def step(self) -> None: def _find_best_nom_fiat_trade(self) -> Optional[Tuple[Dec, Dec]]: trade_price_quant = None - for bid in self.model.market_manager.nomin_fiat_market.highest_bids(): + for bid in self.nomin_fiat_market.highest_bids(): if bid.price < self._nomin_sell_rate_threshold: break if trade_price_quant is not None: @@ -74,7 +74,7 @@ def _make_nom_fiat_trade(self, trade_price_quant: Tuple[Dec, Dec]) -> "ob.Ask": def _find_best_fiat_nom_trade(self) -> Optional[Tuple[Dec, Dec]]: trade_price_quant = None - for ask in self.model.market_manager.nomin_fiat_market.lowest_asks(): + for ask in self.nomin_fiat_market.lowest_asks(): if ask.price > self._nomin_buy_rate_threshold: break if trade_price_quant is not None: diff --git a/simulation/agents/randomizer.py b/simulation/agents/randomizer.py index b47c2db..ffa17d8 100644 --- a/simulation/agents/randomizer.py +++ b/simulation/agents/randomizer.py @@ -45,31 +45,31 @@ def step(self) -> None: break def _curit_fiat_bid_(self) -> "ob.Bid": - price = self.model.market_manager.curit_fiat_market.price + price = self.curit_fiat_market.price movement = hm.round_decimal(Dec(2*random.random() - 1) * price * self.variance) return self.place_curit_fiat_bid(self._fraction_(self.available_fiat, Dec(10)), price + movement) def _curit_fiat_ask_(self) -> "ob.Ask": - price = self.model.market_manager.curit_fiat_market.price + price = self.curit_fiat_market.price movement = hm.round_decimal(Dec(2*random.random() - 1) * price * self.variance) return self.place_curit_fiat_ask(self._fraction_(self.available_curits, Dec(10)), price + movement) def _nomin_fiat_bid_(self) -> "ob.Bid": - price = self.model.market_manager.nomin_fiat_market.price + 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 _nomin_fiat_ask_(self) -> "ob.Ask": - price = self.model.market_manager.nomin_fiat_market.price + price = self.nomin_fiat_market.price movement = hm.round_decimal(Dec(2*random.random() - 1) * price * self.variance) return self.place_nomin_fiat_ask(self._fraction_(self.available_nomins, Dec(10)), price + movement) def _curit_nomin_bid_(self) -> "ob.Bid": - price = self.model.market_manager.curit_nomin_market.price + price = self.curit_nomin_market.price movement = hm.round_decimal(Dec(2*random.random() - 1) * price * self.variance) return self.place_curit_nomin_bid(self._fraction_(self.available_nomins, Dec(10)), price + movement) def _curit_nomin_ask_(self) -> "ob.Ask": - price = self.model.market_manager.curit_nomin_market.price + price = self.curit_nomin_market.price movement = hm.round_decimal(Dec(2*random.random() - 1) * price * self.variance) return self.place_curit_nomin_ask(self._fraction_(self.available_curits, Dec(10)), price + movement) diff --git a/simulation/managers/agentmanager.py b/simulation/managers/agentmanager.py index 39f7baa..91ae5a9 100644 --- a/simulation/managers/agentmanager.py +++ b/simulation/managers/agentmanager.py @@ -32,7 +32,7 @@ def __init__(self, total_value = sum(agent_fractions.values()) if total_value > 0: for k in agent_fractions: - agent_fractions[k] / total_value + agent_fractions[k] /= total_value # Set the actual number of each agent type. num_bankers = int(num_agents * agent_fractions[ag.Banker]) \ @@ -83,7 +83,7 @@ def __init__(self, central_bank = ag.CentralBank( i, self.havven, fiat=Dec(num_agents * init_value), - nomin_target=Dec('1.0') + curit_target=Dec('1.0') ) self.havven.endow_curits(central_bank, Dec(num_agents * init_value)) diff --git a/simulation/managers/marketmanager.py b/simulation/managers/marketmanager.py index b707257..05bc7a4 100644 --- a/simulation/managers/marketmanager.py +++ b/simulation/managers/marketmanager.py @@ -26,18 +26,24 @@ def __init__(self, model_manager: HavvenManager, fee_manager: FeeManager) -> Non model_manager, "curits", "nomins", self.curit_nomin_match, self.fee_manager.transferred_nomins_fee, self.fee_manager.transferred_curits_fee, + self.fee_manager.transferred_nomins_received, + self.fee_manager.transferred_curits_received, self.model_manager.match_on_order ) self.curit_fiat_market = ob.OrderBook( model_manager, "curits", "fiat", self.curit_fiat_match, self.fee_manager.transferred_fiat_fee, self.fee_manager.transferred_curits_fee, + self.fee_manager.transferred_fiat_received, + self.fee_manager.transferred_curits_received, self.model_manager.match_on_order ) self.nomin_fiat_market = ob.OrderBook( model_manager, "nomins", "fiat", self.nomin_fiat_match, self.fee_manager.transferred_fiat_fee, self.fee_manager.transferred_nomins_fee, + self.fee_manager.transferred_fiat_received, + self.fee_manager.transferred_nomins_received, self.model_manager.match_on_order ) diff --git a/simulation/model.py b/simulation/model.py index 9aafb34..570f7ed 100644 --- a/simulation/model.py +++ b/simulation/model.py @@ -53,8 +53,8 @@ def __init__(self, num_agents: int, init_value: float = 1000.0, self.agent_manager = AgentManager(self, num_agents, fractions, Dec(init_value)) - def fiat_value(self, curits = Dec('0'), nomins = Dec('0'), - fiat = Dec('0')) -> Dec: + def fiat_value(self, curits=Dec('0'), nomins=Dec('0'), + fiat=Dec('0')) -> Dec: """Return the equivalent fiat value of the given currency basket.""" return self.market_manager.curits_to_fiat(curits) + \ self.market_manager.nomins_to_fiat(nomins) + fiat diff --git a/simulation/orderbook.py b/simulation/orderbook.py index 137c96b..f7e4157 100644 --- a/simulation/orderbook.py +++ b/simulation/orderbook.py @@ -161,19 +161,51 @@ def __str__(self) -> str: class OrderBook: """ An order book for Havven agents to interact with. - This one is generic, but there will have to be a market for each currency pair. + + The order book will handle trades between a particular currency pair, + consisting of the "base" and "quoted" currencies. + This is generic; there will have to be a book for each pair. + + The book holds two lists of orders, asks and bids. Each order has a price, + quantity, and time of issue. They are ordered in the book by price, and then + by time. + An ask is an order to sell the base currency, while a bid is an order to buy it. + Therefore, merchants filing asks hold the base currency, while those filing bids + hold the quoted currency. + Order prices are a quantity of the quoted currency per unit of the base currency. + Order quantities shall be a quantity of the base currency to trade. + + If a bid has a higher price than an ask, then the orders may match, + in which case the issuers trade at the price on the earlier-issued order. + The seller will transfer the smaller quantity of the base currency + to the buyer, while the buyer will transfer that quantity times the match price + of the quoted currency to the seller. + + An order may be partially filled, in which case its quantity will be decreased. + If an order's remaining quantity falls to zero, the order is completely filled + and struck off the book. + A user may cancel bids they have issued at any time. + + If an ask is placed at a price lower or equal to the highest bid price, it + will be immediately matched against the most favourable orders in turn until it is completely + filled (if possible). + The operation is symmetric if a bid is placed at a price higher than the lowest + ask price. """ def __init__(self, model_manager: "HavvenManager", base: str, quote: str, - matcher: Matcher, bid_fee_fn: Callable[[Dec], Dec], + matcher: Matcher, + bid_fee_fn: Callable[[Dec], Dec], ask_fee_fn: Callable[[Dec], Dec], + bid_qty_received_fn: Callable[[Dec], Dec], + ask_qty_received_fn: Callable[[Dec], Dec], match_on_order: bool = True) -> None: # hold onto the model to be able to access variables self.model_manager = model_manager # Define the currency pair held by this book. self.base: str = base - self.quote: str = quote + self.quoted: str = quote # Buys and sells should be ordered, by price first, then date. # Bids are ordered highest-first @@ -199,6 +231,8 @@ def __init__(self, model_manager: "HavvenManager", base: str, quote: str, # Fees will be calculated with the following functions. self._bid_fee_fn_: Callable[[Dec], Dec] = bid_fee_fn self._ask_fee_fn_: Callable[[Dec], Dec] = ask_fee_fn + self._bid_qty_received_fn_: Callable[[Dec], Dec] = bid_qty_received_fn + self._ask_qty_received_fn_: Callable[[Dec], Dec] = ask_qty_received_fn # A list of all successful trades. self.history: List[TradeRecord] = [] @@ -211,7 +245,7 @@ def name(self) -> str: """ Return this market's name. """ - return f"{self.base}/{self.quote}" + return f"{self.base}/{self.quoted}" def step(self) -> None: """ @@ -259,33 +293,48 @@ def _ask_bucket_deduct_(self, price: Dec, quantity: Dec) -> None: else: self.ask_price_buckets[price] -= quantity - def _bid_fee_(self, price: Dec, quantity: Dec) -> Dec: + def bid_fee(self, price: Dec, quantity: Dec) -> Dec: """ Return the fee paid on the quoted end for a bid of the given quantity and price. """ return self._bid_fee_fn_(HavvenManager.round_decimal(price * quantity)) - def _ask_fee_(self, price: Dec, quantity: Dec) -> Dec: + def ask_fee(self, price: Dec, quantity: Dec) -> Dec: """ Return the fee paid on the base end for an ask of the given quantity and price. """ return self._ask_fee_fn_(quantity) + def ask_sent_quantity(self, price: Dec, quantity: Dec) -> Dec: + """ + The quantity of the base currency received by a buyer (fees deducted). + """ + return self._ask_qty_received_fn_(quantity) + + def bid_sent_quantity(self, price: Dec, quantity: Dec) -> Dec: + """ + The quantity of the quoted currency received by a seller (fees deducted). + """ + return self._bid_qty_received_fn_(HavvenManager.round_decimal(price*quantity)) + def bid(self, price: Dec, quantity: Dec, agent: "ag.MarketPlayer") -> Optional[Bid]: """ Submit a new sell order to the book. """ - fee = self._bid_fee_(price, quantity) + if quantity == 0: + return None + + fee = self.bid_fee(price, quantity) # Fail if the value of the order exceeds the agent's available supply - if agent.__getattribute__(f"available_{self.quote}") < HavvenManager.round_decimal(price*quantity) + fee: + if agent.__getattribute__(f"available_{self.quoted}") < HavvenManager.round_decimal(quantity) + fee: return None bid = Bid(price, quantity, fee, agent, self) - # Attempt to trde the bid immediately if desired. + # Attempt to trade the bid immediately if desired. if self.match_on_order: self.match() @@ -295,7 +344,11 @@ def ask(self, price: Dec, quantity: Dec, agent: "ag.MarketPlayer") -> Optional[A """ Submit a new buy order to the book. """ - fee = self._ask_fee_(price, quantity) + + if quantity == 0: + return None + + fee = self.ask_fee(price, quantity) # Fail if the value of the order exceeds the agent's available supply if agent.__getattribute__(f"available_{self.base}") < quantity + fee: @@ -303,23 +356,23 @@ def ask(self, price: Dec, quantity: Dec, agent: "ag.MarketPlayer") -> Optional[A ask = Ask(price, quantity, fee, agent, self) - # Attempt to trde the ask immediately if desired. + # Attempt to trade the ask immediately if desired. if self.match_on_order: self.match() return ask - def buy(self, quantity: Dec, agent: "ag.MarketPlayer", premium: Dec = Dec('0.0')) -> Bid: + def buy(self, quantity: Dec, agent: "ag.MarketPlayer", premium: Dec = Dec('0.0')) -> Optional[Bid]: """ - Buy a quantity of the sale token at the best available price. + Buy a quantity of the base currency at the best available price. Optionally buy at a premium a certain fraction above the market price. """ price = HavvenManager.round_decimal(self.price_to_buy_quantity(quantity) * (Dec(1) + premium)) return self.bid(price, quantity, agent) - def sell(self, quantity: Dec, agent: "ag.MarketPlayer", discount: Dec = Dec('0.0')) -> Ask: + def sell(self, quantity: Dec, agent: "ag.MarketPlayer", discount: Dec = Dec('0.0')) -> Optional[Ask]: """ - Sell a quantity of the sale token at the best available price. + Sell a quantity of the base currency at the best available price. Optionally sell at a discount a certain fraction below the market price. """ price = HavvenManager.round_decimal(self.price_to_sell_quantity(quantity) * (Dec(1) - discount)) @@ -357,7 +410,38 @@ def price_to_sell_quantity(self, quantity: Dec) -> Dec: break return price - def bids_higher_or_equal(self, price: Dec) -> Iterable[Bid]: + 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 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 bids_not_lower(self, price: Dec) -> Iterable[Bid]: """ Return an iterator of bids whose prices are no lower than the given price. """ @@ -373,9 +457,16 @@ def highest_bids(self) -> Iterable[Bid]: """ Return the list of highest-priced bids. May be empty if there are none. """ - return self.bids_higher_or_equal(self.highest_bid_price()) + return self.bids_not_lower(self.highest_bid_price()) - def asks_lower_or_equal(self, price: Dec) -> Iterable[Bid]: + def highest_bid_quantity(self) -> Dec: + """ + Return the quantity of the base currency demanded at the highest bid price. + """ + # Enclose in Decimal constructor in case sum is 0. + return Dec(sum(b.quantity for b in self.highest_bids())) + + def asks_not_higher(self, price: Dec) -> Iterable[Bid]: """ Return an iterator of asks whose prices are no higher than the given price. """ @@ -391,7 +482,14 @@ def lowest_asks(self) -> Iterable[Bid]: """ Return the list of lowest-priced asks. May be empty if there are none. """ - return self.asks_lower_or_equal(self.lowest_ask_price()) + return self.asks_not_higher(self.lowest_ask_price()) + + def lowest_ask_quantity(self) -> Dec: + """ + Return the quantity of the base currency supplied at the lowest ask price. + """ + # Enclose in Decimal constructor in case sum is 0. + return Dec(sum(a.quantity for a in self.lowest_asks())) def spread(self) -> Dec: """ @@ -412,7 +510,7 @@ def _add_new_bid_(self, bid: Bid) -> None: return # Update the issuer's unavailable quote value. - bid.issuer.__dict__[f"unavailable_{self.quote}"] += bid.quantity + bid.fee + bid.issuer.__dict__[f"unavailable_{self.quoted}"] += (bid.quantity*bid.price) + bid.fee # Add to the issuer and book's records bid.issuer.orders.add(bid) @@ -444,8 +542,11 @@ def update_bid(self, bid: Bid, # Do nothing if the price and quantity would remain unchanged. if bid.price == new_price and bid.quantity == new_quantity: - if fee is None or fee == bid.fee: + if fee == bid.fee or fee is None: return + else: + print(bid) + raise Exception("Fee changed, but price and quantity are unchanged...") # If the bid is updated with a non-positive quantity, it is cancelled. if new_quantity <= 0: @@ -455,11 +556,11 @@ def update_bid(self, bid: Bid, # Compute the new fee. new_fee = fee if fee is None: - new_fee = self._bid_fee_(new_price, new_quantity) + new_fee = self.bid_fee(new_price, new_quantity) # Update the unavailable quantities for this bid, # deducting the old and crediting the new. - bid.issuer.__dict__[f"unavailable_{self.quote}"] += (HavvenManager.round_decimal(new_quantity*new_price) + new_fee) - \ + bid.issuer.__dict__[f"unavailable_{self.quoted}"] += (HavvenManager.round_decimal(new_quantity*new_price) + new_fee) - \ (HavvenManager.round_decimal(bid.quantity*bid.price) + bid.fee) if bid.price == new_price: @@ -500,7 +601,7 @@ def cancel_bid(self, bid: Bid) -> None: return # Free up tokens occupied by this bid. - bid.issuer.__dict__[f"unavailable_{self.quote}"] -= bid.quantity + bid.fee + bid.issuer.__dict__[f"unavailable_{self.quoted}"] -= bid.quantity*bid.price + bid.fee # Remove this order's remaining quantity from its price bucket self._bid_bucket_deduct_(bid.price, bid.quantity) @@ -559,6 +660,9 @@ def update_ask(self, ask: Ask, 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: @@ -568,7 +672,7 @@ def update_ask(self, ask: Ask, # Compute the new fee new_fee = fee if fee is None: - new_fee = self._ask_fee_(new_price, new_quantity) + new_fee = self.ask_fee(new_price, new_quantity) # Update the unavailable quantities for this ask, # deducting the old and crediting the new. @@ -643,6 +747,8 @@ def match(self) -> None: # If a trade was made, then save it in the history. if trade is not None: self.history.append(trade) + trade.seller.notify_trade(trade) + trade.buyer.notify_trade(trade) spread = self.spread()