diff --git a/mango/marketmaking/desiredorder.py b/mango/marketmaking/desiredorder.py new file mode 100644 index 0000000..4eba036 --- /dev/null +++ b/mango/marketmaking/desiredorder.py @@ -0,0 +1,38 @@ +# # ⚠ Warning +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# [πŸ₯­ Mango Markets](https://mango.markets/) support is available at: +# [Docs](https://docs.mango.markets/) +# [Discord](https://discord.gg/67jySBhxrg) +# [Twitter](https://twitter.com/mangomarkets) +# [Github](https://github.com/blockworks-foundation) +# [Email](mailto:hello@blockworks.foundation) + + +import mango + +from decimal import Decimal + + +# # πŸ₯­ DesiredOrder class +# +# Encapsulates a single order we want to be present on the orderbook. +# + +class DesiredOrder: + def __init__(self, side: mango.Side, order_type: mango.OrderType, price: Decimal, quantity: Decimal): + self.side: mango.Side = side + self.order_type: mango.OrderType = order_type + self.price: Decimal = price + self.quantity: Decimal = quantity + + def __str__(self) -> str: + return f"""Β« π™³πšŽπšœπš’πš›πšŽπšπ™Ύπš›πšπšŽπš›: {self.order_type} - {self.side} {self.quantity} at {self.price} Β»""" + + def __repr__(self) -> str: + return f"{self}" diff --git a/mango/marketmaking/desiredordersbuilder.py b/mango/marketmaking/desiredordersbuilder.py new file mode 100644 index 0000000..0848694 --- /dev/null +++ b/mango/marketmaking/desiredordersbuilder.py @@ -0,0 +1,58 @@ +# # ⚠ Warning +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# [πŸ₯­ Mango Markets](https://mango.markets/) support is available at: +# [Docs](https://docs.mango.markets/) +# [Discord](https://discord.gg/67jySBhxrg) +# [Twitter](https://twitter.com/mangomarkets) +# [Github](https://github.com/blockworks-foundation) +# [Email](mailto:hello@blockworks.foundation) + + +import abc +import logging +import mango +import typing + +from .desiredorder import DesiredOrder +from .modelstate import ModelState + + +# # πŸ₯­ DesiredOrdersBuilder class +# +# A builder that builds a list of orders we'd like to be on the orderbook. +# +# The logic of what orders to create will be implemented in a derived class. +# + +class DesiredOrdersBuilder(metaclass=abc.ABCMeta): + def __init__(self): + self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) + + @abc.abstractmethod + def build(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[DesiredOrder]: + raise NotImplementedError("DesiredOrdersBuilder.build() is not implemented on the base type.") + + def __repr__(self) -> str: + return f"{self}" + + +# # πŸ₯­ NullDesiredOrdersBuilder class +# +# A no-op implementation of the `DesiredOrdersBuilder` that will never ask to create orders. +# + +class NullDesiredOrdersBuilder(DesiredOrdersBuilder): + def __init__(self): + super().__init__() + + def build(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[DesiredOrder]: + return [] + + def __str__(self) -> str: + return f"Β« π™½πšžπš•πš•π™³πšŽπšœπš’πš›πšŽπšπ™Ύπš›πšπšŽπš›πšœπ™±πšžπš’πš•πšπšŽπš› Β»" diff --git a/mango/marketmaking/fixedratiodesiredordersbuilder.py b/mango/marketmaking/fixedratiodesiredordersbuilder.py new file mode 100644 index 0000000..249a45d --- /dev/null +++ b/mango/marketmaking/fixedratiodesiredordersbuilder.py @@ -0,0 +1,65 @@ +# # ⚠ Warning +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# [πŸ₯­ Mango Markets](https://mango.markets/) support is available at: +# [Docs](https://docs.mango.markets/) +# [Discord](https://discord.gg/67jySBhxrg) +# [Twitter](https://twitter.com/mangomarkets) +# [Github](https://github.com/blockworks-foundation) +# [Email](mailto:hello@blockworks.foundation) + + +import logging +import mango +import typing + +from decimal import Decimal + +from .desiredorder import DesiredOrder +from .desiredordersbuilder import DesiredOrdersBuilder +from .modelstate import ModelState + + +# # πŸ₯­ FixedRatioDesiredOrdersBuilder class +# +# Builds orders using a fixed spread ratio and a fixed position size ratio. +# + +class FixedRatioDesiredOrdersBuilder(DesiredOrdersBuilder): + def __init__(self, spread_ratio: Decimal, position_size_ratio: Decimal): + self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) + self.spread_ratio: Decimal = spread_ratio + self.position_size_ratio: Decimal = position_size_ratio + + def build(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[DesiredOrder]: + price: mango.Price = model_state.price + inventory: typing.Sequence[typing.Optional[mango.TokenValue]] = model_state.account.net_assets + base_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.base) + if base_tokens is None: + raise Exception(f"Could not find market-maker base token {price.market.base.symbol} in inventory.") + + quote_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.quote) + if quote_tokens is None: + raise Exception(f"Could not find market-maker quote token {price.market.quote.symbol} in inventory.") + + total = (base_tokens.value * price.mid_price) + quote_tokens.value + position_size = total * self.position_size_ratio + + buy_size: Decimal = position_size / price.mid_price + sell_size: Decimal = position_size / price.mid_price + + bid: Decimal = price.mid_price - (price.mid_price * self.spread_ratio) + ask: Decimal = price.mid_price + (price.mid_price * self.spread_ratio) + + return [ + DesiredOrder(mango.Side.BUY, mango.OrderType.POST_ONLY, bid, buy_size), + DesiredOrder(mango.Side.SELL, mango.OrderType.POST_ONLY, ask, sell_size) + ] + + def __str__(self) -> str: + return f"Β« π™΅πš’πš‘πšŽπšπšπšŠπšπš’πš˜π™³πšŽπšœπš’πš›πšŽπšπ™Ύπš›πšπšŽπš›πšœπ™±πšžπš’πš•πšπšŽπš› using ratios - spread: {self.spread_ratio}, position size: {self.position_size_ratio} Β»" diff --git a/mango/marketmaking/marketmaker.py b/mango/marketmaking/marketmaker.py index e2dd151..41fe121 100644 --- a/mango/marketmaking/marketmaker.py +++ b/mango/marketmaking/marketmaker.py @@ -15,13 +15,14 @@ import logging - import mango import traceback import typing from decimal import Decimal -from mango.marketmaking.modelstate import ModelState + +from .desiredordersbuilder import DesiredOrdersBuilder +from .modelstate import ModelState # # πŸ₯­ MarketMaker class @@ -32,45 +33,20 @@ from mango.marketmaking.modelstate import ModelState class MarketMaker: def __init__(self, wallet: mango.Wallet, market: mango.Market, market_instruction_builder: mango.MarketInstructionBuilder, - spread_ratio: Decimal, position_size_ratio: Decimal): + desired_orders_builder: DesiredOrdersBuilder): self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.wallet: mango.Wallet = wallet self.market: mango.Market = market self.market_instruction_builder: mango.MarketInstructionBuilder = market_instruction_builder - self.spread_ratio: Decimal = spread_ratio - self.position_size_ratio: Decimal = position_size_ratio + self.desired_orders_builder: DesiredOrdersBuilder = desired_orders_builder + self.buy_client_ids: typing.List[int] = [] self.sell_client_ids: typing.List[int] = [] - def calculate_order_prices(self, model_state: ModelState) -> typing.Tuple[Decimal, Decimal]: - price: mango.Price = model_state.price - bid: Decimal = price.mid_price - (price.mid_price * self.spread_ratio) - ask: Decimal = price.mid_price + (price.mid_price * self.spread_ratio) - - return (bid, ask) - - def calculate_order_sizes(self, model_state: ModelState) -> typing.Tuple[Decimal, Decimal]: - price: mango.Price = model_state.price - inventory: typing.Sequence[typing.Optional[mango.TokenValue]] = model_state.account.net_assets - base_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.base) - if base_tokens is None: - raise Exception(f"Could not find market-maker base token {price.market.base.symbol} in inventory.") - - quote_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.quote) - if quote_tokens is None: - raise Exception(f"Could not find market-maker quote token {price.market.quote.symbol} in inventory.") - - total = (base_tokens.value * price.mid_price) + quote_tokens.value - position_size = total * self.position_size_ratio - - buy_size: Decimal = position_size / price.mid_price - sell_size: Decimal = position_size / price.mid_price - return (buy_size, sell_size) - def pulse(self, context: mango.Context, model_state: ModelState): try: - bid, ask = self.calculate_order_prices(model_state) - buy_size, sell_size = self.calculate_order_sizes(model_state) + desired_orders = self.desired_orders_builder.build(context, model_state) + payer = mango.CombinableInstructions.from_wallet(self.wallet) cancellations = mango.CombinableInstructions.empty() @@ -83,22 +59,24 @@ class MarketMaker: cancel = self.market_instruction_builder.build_cancel_order_instructions(order) cancellations += cancel - buy_client_id = context.random_client_id() - self.buy_client_ids += [buy_client_id] - self.logger.info(f"Placing BUY order for {buy_size} at price {bid} with client ID: {buy_client_id}") - buy = self.market_instruction_builder.build_place_order_instructions( - mango.Side.BUY, mango.OrderType.POST_ONLY, bid, buy_size, buy_client_id) + place_orders = mango.CombinableInstructions.empty() + for desired_order in desired_orders: + client_id = context.random_client_id() + if desired_order.side == mango.Side.BUY: + self.buy_client_ids += [client_id] + else: + self.sell_client_ids += [client_id] - sell_client_id = context.random_client_id() - self.sell_client_ids += [sell_client_id] - self.logger.info(f"Placing SELL order for {sell_size} at price {ask} with client ID: {sell_client_id}") - sell = self.market_instruction_builder.build_place_order_instructions( - mango.Side.SELL, mango.OrderType.POST_ONLY, ask, sell_size, sell_client_id) + self.logger.info( + f"Placing {desired_order.side} order for {desired_order.quantity} at price {desired_order.price} with client ID: {client_id}") + place_order = self.market_instruction_builder.build_place_order_instructions( + desired_order.side, desired_order.order_type, desired_order.price, desired_order.quantity, client_id) + place_orders += place_order settle = self.market_instruction_builder.build_settle_instructions() crank = self.market_instruction_builder.build_crank_instructions() - (payer + cancellations + buy + sell + settle + crank).execute(context) + (payer + cancellations + place_orders + settle + crank).execute(context) except Exception as exception: self.logger.error(f"Market-maker error on pulse: {exception} - {traceback.format_exc()}")