mango-explorer/mango/simplemarketmaking/simplemarketmaker.py

259 lines
10 KiB
Python

# # ⚠ 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 time
import traceback
import typing
from datetime import timedelta
from decimal import Decimal
from pathlib import Path
# # 🥭 SimpleMarketMaker class
#
# This is a simple demonstration of market making. It is intended to show how to do some things market
# makers require. It is not intended to be an actual, useful market maker.
#
# This market maker performs the following steps:
#
# 1. Cancel any orders
# 2. Update current state
# 2a. Fetch current prices
# 2b. Fetch current inventory
# 3. Figure out what orders to place
# 4. Place those orders
# 5. Sleep for a defined period
# 6. Repeat from Step 1
#
# There are many features missing that you'd expect in a more realistic market maker. Here are just a few:
# * There is very little error handling
# * There is no retrying of failed actions
# * There is no introspection on whether orders are filled
# * There is no inventory management, nor any attempt to balance number of filled buys with number of
# filled sells.
# * Token prices and quantities are rounded to the token mint's decimals, not the market's tick size and
# lot size
# * The strategy of placing orders at a fixed spread around the mid price without taking any other factors
# into account is likely to be costly
# * Place and Cancel instructions aren't batched into single transactions
#
class SimpleMarketMaker:
def __init__(
self,
context: mango.Context,
wallet: mango.Wallet,
market: mango.SerumMarket,
market_operations: mango.MarketOperations,
oracle: mango.Oracle,
spread_ratio: Decimal,
position_size_ratio: Decimal,
existing_order_tolerance: Decimal,
pause: timedelta,
) -> None:
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.context: mango.Context = context
self.wallet: mango.Wallet = wallet
self.market: mango.SerumMarket = market
self.market_operations: mango.MarketOperations = market_operations
self.oracle: mango.Oracle = oracle
self.spread_ratio: Decimal = spread_ratio
self.position_size_ratio: Decimal = position_size_ratio
self.existing_order_tolerance: Decimal = existing_order_tolerance
self.pause: timedelta = pause
self.stop_requested = False
self.health_filename = "/var/tmp/mango_healthcheck_simple_market_maker"
def start(self) -> None:
# On startup there should be no existing orders. If we didn't exit cleanly last time though,
# there may still be some hanging around. Cancel any existing orders so we start fresh.
self.cleanup()
while not self.stop_requested:
self._logger.info("Starting fresh iteration.")
try:
# Update current state
price = self.oracle.fetch_price(self.context)
self._logger.info(f"Price is: {price}")
inventory = self.fetch_inventory()
# Calculate what we want the orders to be.
bid, ask = self.calculate_order_prices(price)
buy_quantity, sell_quantity = self.calculate_order_quantities(
price, inventory
)
current_orders = self.market_operations.load_my_orders(
include_expired=True
)
buy_orders = [
order for order in current_orders if order.side == mango.Side.BUY
]
if self.orders_require_action(buy_orders, bid, buy_quantity):
self._logger.info("Cancelling BUY orders.")
for order in buy_orders:
self.market_operations.cancel_order(order)
buy_order: mango.Order = mango.Order.from_values(
mango.Side.BUY, bid, buy_quantity, mango.OrderType.POST_ONLY
)
self.market_operations.place_order(buy_order)
sell_orders = [
order for order in current_orders if order.side == mango.Side.SELL
]
if self.orders_require_action(sell_orders, ask, sell_quantity):
self._logger.info("Cancelling SELL orders.")
for order in sell_orders:
self.market_operations.cancel_order(order)
sell_order: mango.Order = mango.Order.from_values(
mango.Side.SELL, ask, sell_quantity, mango.OrderType.POST_ONLY
)
self.market_operations.place_order(sell_order)
self.update_health_on_successful_iteration()
except Exception as exception:
self._logger.warning(
f"Pausing and continuing after problem running market-making iteration: {exception} - {traceback.format_exc()}"
)
# Wait and hope for fills.
self._logger.info(f"Pausing for {self.pause} seconds.")
time.sleep(self.pause.seconds)
self._logger.info("Stopped.")
self.cleanup()
def stop(self) -> None:
self._logger.info("Stop requested.")
self.stop_requested = True
Path(self.health_filename).unlink(missing_ok=True)
def cleanup(self) -> None:
self._logger.info("Cleaning up.")
orders = self.market_operations.load_my_orders(include_expired=True)
for order in orders:
self.market_operations.cancel_order(order)
def fetch_inventory(
self,
) -> typing.Sequence[typing.Optional[mango.InstrumentValue]]:
if self.market.inventory_source == mango.InventorySource.SPL_TOKENS:
base_account = mango.TokenAccount.fetch_largest_for_owner_and_token(
self.context, self.wallet.address, self.market.base
)
if base_account is None:
raise Exception(
f"Could not find token account owned by {self.wallet.address} for base token {self.market.base}."
)
quote_account = mango.TokenAccount.fetch_largest_for_owner_and_token(
self.context, self.wallet.address, self.market.quote
)
if quote_account is None:
raise Exception(
f"Could not find token account owned by {self.wallet.address} for quote token {self.market.quote}."
)
return [base_account.value, quote_account.value]
else:
group = mango.Group.load(self.context)
accounts = mango.Account.load_all_for_owner(
self.context, self.wallet.address, group
)
if len(accounts) == 0:
raise Exception("No Mango account found.")
account = accounts[0]
return account.net_values_by_index
def calculate_order_prices(
self, price: mango.Price
) -> typing.Tuple[Decimal, Decimal]:
bid = price.mid_price - (price.mid_price * self.spread_ratio)
ask = price.mid_price + (price.mid_price * self.spread_ratio)
return (bid, ask)
def calculate_order_quantities(
self,
price: mango.Price,
inventory: typing.Sequence[typing.Optional[mango.InstrumentValue]],
) -> typing.Tuple[Decimal, Decimal]:
base_tokens: typing.Optional[
mango.InstrumentValue
] = mango.InstrumentValue.find_by_token(inventory, self.market.base)
if base_tokens is None:
raise Exception(
f"Could not find market-maker base token {self.market.base.symbol} in inventory."
)
quote_tokens: typing.Optional[
mango.InstrumentValue
] = mango.InstrumentValue.find_by_token(inventory, self.market.quote)
if quote_tokens is None:
raise Exception(
f"Could not find market-maker quote token {self.market.quote.symbol} in inventory."
)
buy_quantity = base_tokens.value * self.position_size_ratio
sell_quantity = (
quote_tokens.value / price.mid_price
) * self.position_size_ratio
return (buy_quantity, sell_quantity)
def orders_require_action(
self, orders: typing.Sequence[mango.Order], price: Decimal, quantity: Decimal
) -> bool:
def within_tolerance(
target_value: Decimal, order_value: Decimal, tolerance: Decimal
) -> bool:
tolerated = order_value * tolerance
return bool(
(order_value < (target_value + tolerated))
and (order_value > (target_value - tolerated))
)
return len(orders) == 0 or not all(
[
(
not order.expired
and within_tolerance(
price, order.price, self.existing_order_tolerance
)
and within_tolerance(
quantity, order.quantity, self.existing_order_tolerance
)
)
for order in orders
]
)
def update_health_on_successful_iteration(self) -> None:
try:
Path(self.health_filename).touch(mode=0o666, exist_ok=True)
except Exception as exception:
self._logger.warning(
f"Touching file '{self.health_filename}' raised exception: {exception}"
)
def __str__(self) -> str:
return f"""« SimpleMarketMaker for market '{self.market.fully_qualified_symbol}' »"""
def __repr__(self) -> str:
return f"{self}"