mango-explorer/mango/marketmaking/modelstatebuilder.py

337 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# # ⚠ 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 time
import typing
from decimal import Decimal
from solana.publickey import PublicKey
from ..modelstate import ModelState
from ..tokenvalue import TokenValue
from ..calculators.collateralcalculator import CollateralCalculator
from ..calculators.perpcollateralcalculator import PerpCollateralCalculator
from ..calculators.spotcollateralcalculator import SpotCollateralCalculator
# # 🥭 ModelStateBuilder class
#
# Base class for building a `ModelState` through polling or websockets.
#
class ModelStateBuilder(metaclass=abc.ABCMeta):
def __init__(self):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@abc.abstractmethod
def build(self, context: mango.Context) -> ModelState:
raise NotImplementedError("ModelStateBuilder.build() is not implemented on the base type.")
def __str__(self) -> str:
return "« 𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »"
def __repr__(self) -> str:
return f"{self}"
# # 🥭 WebsocketModelStateBuilder class
#
# Base class for building a `ModelState` through polling.
#
class WebsocketModelStateBuilder(ModelStateBuilder):
def __init__(self, model_state: ModelState):
super().__init__()
self.model_state: ModelState = model_state
def build(self, context: mango.Context) -> ModelState:
return self.model_state
def __str__(self) -> str:
return f"« 𝚆𝚎𝚋𝚜𝚘𝚌𝚔𝚎𝚝𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.model_state.market.symbol}' »"
# # 🥭 PollingModelStateBuilder class
#
# Base class for building a `ModelState` through polling.
#
class PollingModelStateBuilder(ModelStateBuilder):
def __init__(self):
super().__init__()
def build(self, context: mango.Context) -> ModelState:
started_at = time.time()
built: ModelState = self.poll(context)
time_taken = time.time() - started_at
self.logger.debug(f"Poll for model state complete. Time taken: {time_taken:.2f} seconds.")
return built
@abc.abstractmethod
def poll(self, context: mango.Context) -> ModelState:
raise NotImplementedError("PollingModelStateBuilder.poll() is not implemented on the base type.")
def from_values(self, order_owner: PublicKey, market: mango.Market, group: mango.Group, account: mango.Account,
price: mango.Price, placed_orders_container: mango.PlacedOrdersContainer,
inventory: mango.Inventory, bids: typing.Sequence[mango.Order],
asks: typing.Sequence[mango.Order]) -> ModelState:
group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher(group)
account_watcher: mango.ManualUpdateWatcher[mango.Account] = mango.ManualUpdateWatcher(account)
price_watcher: mango.ManualUpdateWatcher[mango.Price] = mango.ManualUpdateWatcher(price)
placed_orders_container_watcher: mango.ManualUpdateWatcher[
mango.PlacedOrdersContainer] = mango.ManualUpdateWatcher(placed_orders_container)
inventory_watcher: mango.ManualUpdateWatcher[mango.Inventory] = mango.ManualUpdateWatcher(inventory)
bids_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(bids)
asks_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(asks)
return ModelState(order_owner, market, group_watcher, account_watcher, price_watcher,
placed_orders_container_watcher, inventory_watcher, bids_watcher, asks_watcher)
def __str__(self) -> str:
return "« 𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »"
# # 🥭 SerumPollingModelStateBuilder class
#
# Polls Solana and builds a `ModelState` for a `SerumMarket`
#
class SerumPollingModelStateBuilder(PollingModelStateBuilder):
def __init__(self,
order_owner: PublicKey,
market: mango.SerumMarket,
oracle: mango.Oracle,
group_address: PublicKey,
account_address: PublicKey,
open_orders_address: PublicKey,
base_inventory_token_account: mango.TokenAccount,
quote_inventory_token_account: mango.TokenAccount,
):
super().__init__()
self.order_owner: PublicKey = order_owner
self.market: mango.SerumMarket = market
self.oracle: mango.Oracle = oracle
self.group_address: PublicKey = group_address
self.account_address: PublicKey = account_address
self.open_orders_address: PublicKey = open_orders_address
self.base_inventory_token_account: mango.TokenAccount = base_inventory_token_account
self.quote_inventory_token_account: mango.TokenAccount = quote_inventory_token_account
def poll(self, context: mango.Context) -> ModelState:
addresses: typing.List[PublicKey] = [
self.group_address,
self.account_address,
self.open_orders_address,
self.base_inventory_token_account.address,
self.quote_inventory_token_account.address,
self.market.underlying_serum_market.state.bids(),
self.market.underlying_serum_market.state.asks()
]
account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses)
group: mango.Group = mango.Group.parse(context, account_infos[0])
account: mango.Account = mango.Account.parse(account_infos[1], group)
placed_orders_container: mango.PlacedOrdersContainer = mango.OpenOrders.parse(
account_infos[2], self.market.base.decimals, self.market.quote.decimals)
# Serum markets don't accrue MNGO liquidity incentives
mngo = group.find_token_info_by_symbol("MNGO").token
mngo_accrued: TokenValue = TokenValue(mngo, Decimal(0))
base_inventory_token_account = mango.TokenAccount.parse(
account_infos[3], self.base_inventory_token_account.value.token)
quote_inventory_token_account = mango.TokenAccount.parse(
account_infos[4], self.quote_inventory_token_account.value.token)
# Both these will have top-of-book at index 0.
bids: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders(
account_infos[5], self.market.underlying_serum_market)
asks: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders(
account_infos[6], self.market.underlying_serum_market)
price: mango.Price = self.oracle.fetch_price(context)
available: Decimal = (base_inventory_token_account.value.value * price.mid_price) + \
quote_inventory_token_account.value.value
available_collateral: TokenValue = TokenValue(quote_inventory_token_account.value.token, available)
inventory: mango.Inventory = mango.Inventory(mango.InventorySource.SPL_TOKENS,
mngo_accrued,
available_collateral,
base_inventory_token_account.value,
quote_inventory_token_account.value)
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, bids, asks)
def __str__(self) -> str:
return f"""« 𝚂𝚎𝚛𝚞𝚖𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »"""
# # 🥭 SpotPollingModelStateBuilder class
#
# Polls Solana and builds a `ModelState` for a `SpotMarket`
#
class SpotPollingModelStateBuilder(PollingModelStateBuilder):
def __init__(self,
order_owner: PublicKey,
market: mango.SpotMarket,
oracle: mango.Oracle,
group_address: PublicKey,
cache_address: PublicKey,
account_address: PublicKey,
open_orders_address: PublicKey,
all_open_orders_addresses: typing.Sequence[PublicKey]
):
super().__init__()
self.order_owner: PublicKey = order_owner
self.market: mango.SpotMarket = market
self.oracle: mango.Oracle = oracle
self.group_address: PublicKey = group_address
self.cache_address: PublicKey = cache_address
self.account_address: PublicKey = account_address
self.open_orders_address: PublicKey = open_orders_address
self.all_open_orders_addresses: typing.Sequence[PublicKey] = all_open_orders_addresses
self.collateral_calculator: CollateralCalculator = SpotCollateralCalculator()
def poll(self, context: mango.Context) -> ModelState:
addresses: typing.List[PublicKey] = [
self.group_address,
self.cache_address,
self.account_address,
self.market.underlying_serum_market.state.bids(),
self.market.underlying_serum_market.state.asks(),
*self.all_open_orders_addresses
]
account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses)
group: mango.Group = mango.Group.parse(context, account_infos[0])
cache: mango.Cache = mango.Cache.parse(account_infos[1])
account: mango.Account = mango.Account.parse(account_infos[2], group)
# Update our stash of OpenOrders addresses for next time, in case new OpenOrders accounts were added
self.all_open_orders_addresses = list([oo for oo in account.spot_open_orders if oo is not None])
spot_open_orders_account_infos_by_address = {
str(account_info.address): account_info for account_info in account_infos[5:]}
all_open_orders: typing.Dict[str, mango.OpenOrders] = {}
for basket_token in account.basket:
if basket_token.spot_open_orders is not None and str(basket_token.spot_open_orders) in spot_open_orders_account_infos_by_address:
account_info: mango.AccountInfo = spot_open_orders_account_infos_by_address[str(
basket_token.spot_open_orders)]
open_orders: mango.OpenOrders = mango.OpenOrders.parse(
account_info,
basket_token.token_info.decimals,
account.shared_quote_token.token_info.token.decimals)
all_open_orders[str(basket_token.spot_open_orders)] = open_orders
placed_orders_container: mango.PlacedOrdersContainer = all_open_orders[str(self.open_orders_address)]
# Spot markets don't accrue MNGO liquidity incentives
mngo = group.find_token_info_by_symbol("MNGO").token
mngo_accrued: TokenValue = TokenValue(mngo, Decimal(0))
base_value = mango.TokenValue.find_by_symbol(account.net_assets, self.market.base.symbol)
quote_value = mango.TokenValue.find_by_symbol(account.net_assets, self.market.quote.symbol)
available_collateral: TokenValue = self.collateral_calculator.calculate(account, all_open_orders, group, cache)
inventory: mango.Inventory = mango.Inventory(mango.InventorySource.ACCOUNT,
mngo_accrued,
available_collateral,
base_value,
quote_value)
# Both these will have top-of-book at index 0.
bids: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders(
account_infos[3], self.market.underlying_serum_market)
asks: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders(
account_infos[4], self.market.underlying_serum_market)
price: mango.Price = self.oracle.fetch_price(context)
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, bids, asks)
def __str__(self) -> str:
return f"""« 𝚂𝚙𝚘𝚝𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »"""
# # 🥭 PerpPollingModelStateBuilder class
#
# Polls Solana and builds a `ModelState` for a `PerpMarket`
#
class PerpPollingModelStateBuilder(PollingModelStateBuilder):
def __init__(self,
order_owner: PublicKey,
market: mango.PerpMarket,
oracle: mango.Oracle,
group_address: PublicKey,
cache_address: PublicKey,
account_address: PublicKey
):
super().__init__()
self.order_owner: PublicKey = order_owner
self.market: mango.PerpMarket = market
self.oracle: mango.Oracle = oracle
self.group_address: PublicKey = group_address
self.cache_address: PublicKey = cache_address
self.account_address: PublicKey = account_address
self.collateral_calculator: CollateralCalculator = PerpCollateralCalculator()
def poll(self, context: mango.Context) -> ModelState:
addresses: typing.List[PublicKey] = [
self.group_address,
self.cache_address,
self.account_address,
self.market.underlying_perp_market.bids,
self.market.underlying_perp_market.asks
]
account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses)
group: mango.Group = mango.Group.parse(context, account_infos[0])
cache: mango.Cache = mango.Cache.parse(account_infos[1])
account: mango.Account = mango.Account.parse(account_infos[2], group)
index = group.find_perp_market_index(self.market.address)
perp_account = account.perp_accounts[index]
if perp_account is None:
raise Exception(f"Could not find perp account at index {index} of account {account.address}.")
placed_orders_container: mango.PlacedOrdersContainer = perp_account.open_orders
base_lots = perp_account.base_position
base_value = self.market.lot_size_converter.base_size_lots_to_number(base_lots)
base_token_value = mango.TokenValue(self.market.base, base_value)
quote_token_value = mango.TokenValue.find_by_symbol(account.net_assets, self.market.quote.symbol)
available_collateral: TokenValue = self.collateral_calculator.calculate(account, {}, group, cache)
inventory: mango.Inventory = mango.Inventory(mango.InventorySource.ACCOUNT,
perp_account.mngo_accrued,
available_collateral,
base_token_value,
quote_token_value)
# Both these will have top-of-book at index 0.
bids: mango.PerpOrderBookSide = mango.PerpOrderBookSide.parse(
context, account_infos[3], self.market.underlying_perp_market)
asks: mango.PerpOrderBookSide = mango.PerpOrderBookSide.parse(
context, account_infos[4], self.market.underlying_perp_market)
price: mango.Price = self.oracle.fetch_price(context)
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, bids.orders(), asks.orders())
def __str__(self) -> str:
return f"""« 𝙿𝚎𝚛𝚙𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »"""