From 8bf638407683cd5f1cb4ac392ec8c5a07b90de1c Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 31 Aug 2021 00:30:22 +0100 Subject: [PATCH] Added CollateralCalculator to calculate collateral in different market types. --- mango/__init__.py | 3 +- mango/account.py | 6 +- mango/accountinfoconverter.py | 1 - mango/collateralcalculator.py | 127 ++++++++++++++++++ mango/group.py | 12 +- mango/inventory.py | 37 +++-- mango/marketmaking/modelstatebuilder.py | 56 +++++--- .../marketmaking/modelstatebuilderfactory.py | 17 ++- .../confidenceintervalspreadelement.py | 6 +- mango/watchers.py | 23 +++- 10 files changed, 240 insertions(+), 48 deletions(-) create mode 100644 mango/collateralcalculator.py diff --git a/mango/__init__.py b/mango/__init__.py index 08f2597..c4f4326 100644 --- a/mango/__init__.py +++ b/mango/__init__.py @@ -8,6 +8,7 @@ from .addressableaccount import AddressableAccount from .balancesheet import BalanceSheet from .cache import PriceCache, RootBankCache, PerpMarketCache, Cache from .client import CompatibleClient, BetterClient +from .collateralcalculator import CollateralCalculator, SerumCollateralCalculator, SpotCollateralCalculator, PerpCollateralCalculator from .combinableinstructions import CombinableInstructions from .constants import SYSTEM_PROGRAM_ADDRESS, SOL_MINT_ADDRESS, SOL_DECIMALS, SOL_DECIMAL_DIVISOR, WARNING_DISCLAIMER_TEXT, MangoConstants from .context import Context @@ -77,7 +78,7 @@ from .version import Version from .wallet import Wallet from .walletbalancer import TargetBalance, FixedTargetBalance, PercentageTargetBalance, TargetBalanceParser, sort_changes_for_trades, calculate_required_balance_changes, FilterSmallChanges, WalletBalancer, NullWalletBalancer, LiveWalletBalancer from .watcher import Watcher, ManualUpdateWatcher, LamdaUpdateWatcher -from .watchers import build_group_watcher, build_account_watcher, build_spot_open_orders_watcher, build_serum_open_orders_watcher, build_perp_open_orders_watcher, build_price_watcher, build_serum_inventory_watcher, build_perp_orderbook_side_watcher, build_serum_orderbook_side_watcher +from .watchers import build_group_watcher, build_account_watcher, build_cache_watcher, build_spot_open_orders_watcher, build_serum_open_orders_watcher, build_perp_open_orders_watcher, build_price_watcher, build_serum_inventory_watcher, build_perp_orderbook_side_watcher, build_serum_orderbook_side_watcher from .websocketsubscription import WebSocketSubscription, WebSocketProgramSubscription, WebSocketAccountSubscription, WebSocketLogSubscription, WebSocketSubscriptionManager, IndividualWebSocketSubscriptionManager, SharedWebSocketSubscriptionManager from .layouts import layouts diff --git a/mango/account.py b/mango/account.py index 59764d6..3b66462 100644 --- a/mango/account.py +++ b/mango/account.py @@ -13,9 +13,6 @@ # [Github](https://github.com/blockworks-foundation) # [Email](mailto:hello@blockworks.foundation) -from mango.perpopenorders import PerpOpenOrders -from mango.placedorder import PlacedOrder -from mango.tokeninfo import TokenInfo import typing from decimal import Decimal @@ -31,7 +28,10 @@ from .layouts import layouts from .metadata import Metadata from .orders import Side from .perpaccount import PerpAccount +from .perpopenorders import PerpOpenOrders +from .placedorder import PlacedOrder from .token import Token +from .tokeninfo import TokenInfo from .tokenvalue import TokenValue from .version import Version diff --git a/mango/accountinfoconverter.py b/mango/accountinfoconverter.py index 01f2efe..2f6ffc5 100644 --- a/mango/accountinfoconverter.py +++ b/mango/accountinfoconverter.py @@ -37,7 +37,6 @@ from .serumeventqueue import SerumEventQueue # Given a `Context` and an account type, returns a function that can take an `AccountInfo` and # return one of our objects. # - def build_account_info_converter(context: Context, account_type: str) -> typing.Callable[[AccountInfo], AddressableAccount]: account_type_upper = account_type.upper() if account_type_upper == "GROUP": diff --git a/mango/collateralcalculator.py b/mango/collateralcalculator.py new file mode 100644 index 0000000..3cd78e0 --- /dev/null +++ b/mango/collateralcalculator.py @@ -0,0 +1,127 @@ +# # ⚠ 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 typing + +from decimal import Decimal + +from .account import Account +from .cache import Cache, PriceCache +from .spotmarketinfo import SpotMarketInfo +from .tokenvalue import TokenValue + + +class CollateralCalculator(metaclass=abc.ABCMeta): + def __init__(self): + self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) + + def calculate(self, account: Account, cache: Cache) -> TokenValue: + raise NotImplementedError("CollateralCalculator.calculate() is not implemented on the base type.") + + +class SerumCollateralCalculator(CollateralCalculator): + def __init__(self): + super().__init__() + + def calculate(self, account: Account, cache: Cache) -> TokenValue: + raise NotImplementedError("SerumCollateralCalculator.calculate() is not implemented.") + + +class SpotCollateralCalculator(CollateralCalculator): + def __init__(self): + super().__init__() + + # From Daffy in Discord, 30th August 2021 (https://discord.com/channels/791995070613159966/807051268304273408/882029587914182666) + # I think the correct calculation is + # total_collateral = deposits[QUOTE_INDEX] * deposit_index - borrows[QUOTE_INDEX] * borrow_index + # for i in num_oracles: + # total_collateral += prices[i] * (init_asset_weights[i] * deposits[i] * deposit_index - init_liab_weights[i] * borrows[i] * borrow_index) + # + # Also from Daffy, same thread, when I said there were two `init_asset_weights`, one for spot and one for perp (https://discord.com/channels/791995070613159966/807051268304273408/882030633940054056): + # yes I think we ignore perps + # + def calculate(self, account: Account, cache: Cache) -> TokenValue: + # Quote token calculation: + # total_collateral = deposits[QUOTE_INDEX] * deposit_index - borrows[QUOTE_INDEX] * borrow_index + # Note: the `AccountBasketToken` in the `Account` already factors the deposit and borrow index. + total: Decimal = account.shared_quote_token.net_value.value + for basket_token in account.basket: + index = account.group.find_base_token_market_index(basket_token.token_info) + token_price: typing.Optional[PriceCache] = cache.price_cache[index] + if token_price is None: + raise Exception( + f"Could not read price of token {basket_token.token_info.token.symbol} at index {index} of cache at {cache.address}") + spot_market: typing.Optional[SpotMarketInfo] = account.group.spot_markets[index] + if spot_market is None: + raise Exception( + f"Could not read spot market of token {basket_token.token_info.token.symbol} at index {index} of cache at {cache.address}") + + # Base token calculations: + # total_collateral += prices[i] * (init_asset_weights[i] * deposits[i] * deposit_index - init_liab_weights[i] * borrows[i] * borrow_index) + # Note: the `AccountBasketToken` in the `Account` already factors the deposit and borrow index. + weighted: Decimal = token_price.price * (( + basket_token.deposit.value * spot_market.init_asset_weight) - ( + basket_token.borrow.value * spot_market.init_liab_weight)) + total += weighted + + return TokenValue(account.group.shared_quote_token.token, total) + + +class PerpCollateralCalculator(CollateralCalculator): + def __init__(self): + super().__init__() + + # From Daffy in Discord, 30th August 2021 (https://discord.com/channels/791995070613159966/807051268304273408/882029587914182666) + # I think the correct calculation is + # total_collateral = deposits[QUOTE_INDEX] * deposit_index - borrows[QUOTE_INDEX] * borrow_index + # for i in num_oracles: + # total_collateral += prices[i] * (init_asset_weights[i] * deposits[i] * deposit_index - init_liab_weights[i] * borrows[i] * borrow_index) + # + # Also from Daffy, same thread, when I said there were two `init_asset_weights`, one for spot and one for perp (https://discord.com/channels/791995070613159966/807051268304273408/882030633940054056): + # yes I think we ignore perps + # + def calculate(self, account: Account, cache: Cache) -> TokenValue: + # Quote token calculation: + # total_collateral = deposits[QUOTE_INDEX] * deposit_index - borrows[QUOTE_INDEX] * borrow_index + # Note: the `AccountBasketToken` in the `Account` already factors the deposit and borrow index. + total: Decimal = account.shared_quote_token.net_value.value + for basket_token in account.basket: + index = account.group.find_base_token_market_index(basket_token.token_info) + token_price: typing.Optional[PriceCache] = cache.price_cache[index] + if token_price is None: + raise Exception( + f"Could not read price of token {basket_token.token_info.token.symbol} at index {index} of cache at {cache.address}") + + # Not using perp market asset weights yet - stick with spot. + # perp_market: typing.Optional[PerpMarketInfo] = account.group.perp_markets[index] + # if perp_market is None: + # raise Exception( + # f"Could not read perp market of token {basket_token.token_info.token.symbol} at index {index} of cache at {cache.address}") + spot_market: typing.Optional[SpotMarketInfo] = account.group.spot_markets[index] + if spot_market is None: + raise Exception( + f"Could not read spot market of token {basket_token.token_info.token.symbol} at index {index} of cache at {cache.address}") + + # Base token calculations: + # total_collateral += prices[i] * (init_asset_weights[i] * deposits[i] * deposit_index - init_liab_weights[i] * borrows[i] * borrow_index) + # Note: the `AccountBasketToken` in the `Account` already factors the deposit and borrow index. + weighted: Decimal = token_price.price * (( + basket_token.deposit.value * spot_market.init_asset_weight) - ( + basket_token.borrow.value * spot_market.init_liab_weight)) + total += weighted + + return TokenValue(account.group.shared_quote_token.token, total) diff --git a/mango/group.py b/mango/group.py index f639dac..139e3d5 100644 --- a/mango/group.py +++ b/mango/group.py @@ -74,7 +74,6 @@ TMappedGroupBasketValue = typing.TypeVar("TMappedGroupBasketValue") # # `Group` defines root functionality for Mango Markets. # - class Group(AddressableAccount): def __init__(self, account_info: AccountInfo, version: Version, name: str, meta_data: Metadata, @@ -122,6 +121,10 @@ class Group(AddressableAccount): def perp_markets(self) -> typing.Sequence[typing.Optional[PerpMarketInfo]]: return Group._map_sequence_to_basket_indices(self.basket, self.basket_indices, lambda item: item.perp_market_info) + @property + def markets(self) -> typing.Sequence[typing.Optional[GroupBasketMarket]]: + return Group._map_sequence_to_basket_indices(self.basket, self.basket_indices, lambda item: item) + @staticmethod def from_layout(context: Context, layout: typing.Any, name: str, account_info: AccountInfo, version: Version, token_lookup: TokenLookup, market_lookup: MarketLookup) -> "Group": meta_data: Metadata = Metadata.from_layout(layout.meta_data) @@ -206,6 +209,13 @@ class Group(AddressableAccount): raise Exception(f"Could not find perp market {perp_market_address} in group {self.address}") + def find_base_token_market_index(self, base_token: TokenInfo) -> int: + for index, bt in enumerate(self.base_tokens): + if bt is not None and bt.token == base_token.token: + return index + + raise Exception(f"Could not find base token {base_token} in group {self.address}") + def find_token_info_by_token(self, token: Token) -> TokenInfo: for token_info in self.tokens: if token_info is not None and token_info.token == token: diff --git a/mango/inventory.py b/mango/inventory.py index 0d65746..88b17b0 100644 --- a/mango/inventory.py +++ b/mango/inventory.py @@ -19,6 +19,8 @@ import logging from decimal import Decimal from .account import Account +from .cache import Cache +from .collateralcalculator import CollateralCalculator, SpotCollateralCalculator, PerpCollateralCalculator from .group import Group from .market import InventorySource, Market from .perpmarket import PerpMarket @@ -30,11 +32,11 @@ from .watcher import Watcher # # This class details inventory of a crypto account for a market. # - class Inventory: - def __init__(self, inventory_source: InventorySource, liquidity_incentives: TokenValue, base: TokenValue, quote: TokenValue): + def __init__(self, inventory_source: InventorySource, liquidity_incentives: TokenValue, available_collateral: TokenValue, base: TokenValue, quote: TokenValue): self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.inventory_source: InventorySource = inventory_source + self.available_collateral: TokenValue = available_collateral self.liquidity_incentives: TokenValue = liquidity_incentives self.base: TokenValue = base self.quote: TokenValue = quote @@ -47,29 +49,34 @@ class Inventory: liquidity_incentives: str = "" if self.liquidity_incentives.value > 0: liquidity_incentives = f" {self.liquidity_incentives}" - return f"Β« π™Έπš—πšŸπšŽπš—πšπš˜πš›πš’ {self.symbol}{liquidity_incentives} [{self.base} / {self.quote}] Β»" + return f"Β« π™Έπš—πšŸπšŽπš—πšπš˜πš›πš’ {self.symbol}{liquidity_incentives} [{self.base} / {self.quote}] ({self.available_collateral} available) Β»" def __repr__(self) -> str: return f"{self}" class SpotInventoryAccountWatcher: - def __init__(self, market: Market, account_watcher: Watcher[Account]): + def __init__(self, market: Market, account_watcher: Watcher[Account], cache_watcher: Watcher[Cache]): self.account_watcher: Watcher[Account] = account_watcher + self.cache_watcher: Watcher[Cache] = cache_watcher account: Account = account_watcher.latest base_value = TokenValue.find_by_symbol(account.net_assets, market.base.symbol) self.base_index: int = account.net_assets.index(base_value) quote_value = TokenValue.find_by_symbol(account.net_assets, market.quote.symbol) self.quote_index: int = account.net_assets.index(quote_value) + self.collateral_calculator: CollateralCalculator = SpotCollateralCalculator() @property def latest(self) -> Inventory: account: Account = self.account_watcher.latest + cache: Cache = self.cache_watcher.latest # Spot markets don't accrue MNGO liquidity incentives mngo = account.group.find_token_info_by_symbol("MNGO").token mngo_accrued: TokenValue = TokenValue(mngo, Decimal(0)) + available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache) + base_value = account.net_assets[self.base_index] if base_value is None: raise Exception( @@ -79,29 +86,35 @@ class SpotInventoryAccountWatcher: raise Exception( f"Could not find net assets in account {account.address} at index {self.quote_index}.") - return Inventory(InventorySource.ACCOUNT, mngo_accrued, base_value, quote_value) + return Inventory(InventorySource.ACCOUNT, mngo_accrued, available_collateral, base_value, quote_value) class PerpInventoryAccountWatcher: - def __init__(self, market: PerpMarket, account_watcher: Watcher[Account], group: Group): + def __init__(self, market: PerpMarket, account_watcher: Watcher[Account], cache_watcher: Watcher[Cache], group: Group): self.market: PerpMarket = market self.account_watcher: Watcher[Account] = account_watcher + self.cache_watcher: Watcher[Cache] = cache_watcher self.perp_account_index: int = group.find_perp_market_index(market.address) account: Account = account_watcher.latest quote_value = TokenValue.find_by_symbol(account.net_assets, market.quote.symbol) self.quote_index: int = account.net_assets.index(quote_value) + self.collateral_calculator: CollateralCalculator = PerpCollateralCalculator() @property def latest(self) -> Inventory: - perp_account = self.account_watcher.latest.perp_accounts[self.perp_account_index] + account: Account = self.account_watcher.latest + cache: Cache = self.cache_watcher.latest + perp_account = account.perp_accounts[self.perp_account_index] if perp_account is None: raise Exception( - f"Could not find perp account for {self.market.symbol} in account {self.account_watcher.latest.address} at index {self.perp_account_index}.") + f"Could not find perp account for {self.market.symbol} in account {account.address} at index {self.perp_account_index}.") + + available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache) + base_lots = perp_account.base_position base_value = self.market.lot_size_converter.quantity_lots_to_value(base_lots) base_token_value = TokenValue(self.market.base, base_value) - quote_token_value = self.account_watcher.latest.net_assets[self.quote_index] + quote_token_value = account.net_assets[self.quote_index] if quote_token_value is None: - raise Exception( - f"Could not find net assets in account {self.account_watcher.latest.address} at index {self.quote_index}.") - return Inventory(InventorySource.ACCOUNT, perp_account.mngo_accrued, base_token_value, quote_token_value) + raise Exception(f"Could not find net assets in account {account.address} at index {self.quote_index}.") + return Inventory(InventorySource.ACCOUNT, perp_account.mngo_accrued, available_collateral, base_token_value, quote_token_value) diff --git a/mango/marketmaking/modelstatebuilder.py b/mango/marketmaking/modelstatebuilder.py index d5217e9..8b6e05a 100644 --- a/mango/marketmaking/modelstatebuilder.py +++ b/mango/marketmaking/modelstatebuilder.py @@ -147,10 +147,6 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder): 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) - inventory: mango.Inventory = mango.Inventory(mango.InventorySource.SPL_TOKENS, - mngo_accrued, - base_inventory_token_account.value, - quote_inventory_token_account.value) bids: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders( account_infos[5], self.market.underlying_serum_market) @@ -159,6 +155,15 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder): 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.market, group, account, price, placed_orders_container, inventory, bids, asks) def __str__(self) -> str: @@ -174,6 +179,7 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): market: mango.SpotMarket, oracle: mango.Oracle, group_address: PublicKey, + cache_address: PublicKey, account_address: PublicKey, open_orders_address: PublicKey ): @@ -182,12 +188,16 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): 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.collateral_calculator: mango.CollateralCalculator = mango.SpotCollateralCalculator() + def poll(self, context: mango.Context) -> ModelState: addresses: typing.List[PublicKey] = [ self.group_address, + self.cache_address, self.account_address, self.open_orders_address, self.market.underlying_serum_market.state.bids(), @@ -195,9 +205,10 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): ] 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) + cache: mango.Cache = mango.Cache.parse(account_infos[1]) + account: mango.Account = mango.Account.parse(account_infos[2], group) placed_orders_container: mango.PlacedOrdersContainer = mango.OpenOrders.parse( - account_infos[2], self.market.base.decimals, self.market.quote.decimals) + account_infos[3], self.market.base.decimals, self.market.quote.decimals) # Spot markets don't accrue MNGO liquidity incentives mngo = account.group.find_token_info_by_symbol("MNGO").token @@ -205,13 +216,18 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): 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) - inventory: mango.Inventory = mango.Inventory( - mango.InventorySource.ACCOUNT, mngo_accrued, base_value, quote_value) + + available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache) + inventory: mango.Inventory = mango.Inventory(mango.InventorySource.ACCOUNT, + mngo_accrued, + available_collateral, + base_value, + quote_value) 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) + asks: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders( + account_infos[5], self.market.underlying_serum_market) price: mango.Price = self.oracle.fetch_price(context) @@ -230,6 +246,7 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder): market: mango.PerpMarket, oracle: mango.Oracle, group_address: PublicKey, + cache_address: PublicKey, account_address: PublicKey ): super().__init__() @@ -237,18 +254,23 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder): 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: mango.CollateralCalculator = mango.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]) - account: mango.Account = mango.Account.parse(account_infos[1], group) + 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] @@ -260,13 +282,17 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder): base_value = self.market.lot_size_converter.quantity_lots_to_value(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) - inventory: mango.Inventory = mango.Inventory( - mango.InventorySource.ACCOUNT, perp_account.mngo_accrued, base_token_value, quote_token_value) + available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache) + inventory: mango.Inventory = mango.Inventory(mango.InventorySource.ACCOUNT, + perp_account.mngo_accrued, + available_collateral, + base_token_value, + quote_token_value) bids: mango.PerpOrderBookSide = mango.PerpOrderBookSide.parse( - context, account_infos[2], self.market.underlying_perp_market) - asks: 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) diff --git a/mango/marketmaking/modelstatebuilderfactory.py b/mango/marketmaking/modelstatebuilderfactory.py index 10fd380..5bce2ee 100644 --- a/mango/marketmaking/modelstatebuilderfactory.py +++ b/mango/marketmaking/modelstatebuilderfactory.py @@ -92,12 +92,12 @@ def _polling_spot_model_state_builder_factory(group: mango.Group, account: mango raise Exception( f"Could not find spot openorders in account {account.address} for market {market.symbol}.") return SpotPollingModelStateBuilder( - market, oracle, group.address, account.address, open_orders_address) + market, oracle, group.address, group.cache, account.address, open_orders_address) def _polling_perp_model_state_builder_factory(group: mango.Group, account: mango.Account, market: mango.PerpMarket, oracle: mango.Oracle) -> ModelStateBuilder: - return PerpPollingModelStateBuilder(market, oracle, group.address, account.address) + return PerpPollingModelStateBuilder(market, oracle, group.address, group.cache, account.address) def _websocket_model_state_builder_factory(context: mango.Context, disposer: mango.DisposePropagator, @@ -118,8 +118,10 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man market = mango.ensure_market_loaded(context, market) if isinstance(market, mango.SerumMarket): + price_watcher: mango.Watcher[mango.Price] = mango.build_price_watcher( + context, websocket_manager, health_check, disposer, "serum", market) inventory_watcher: mango.Watcher[mango.Inventory] = mango.build_serum_inventory_watcher( - context, websocket_manager, health_check, disposer, wallet, market) + context, websocket_manager, health_check, disposer, wallet, market, price_watcher) latest_open_orders_observer: mango.Watcher[mango.PlacedOrdersContainer] = mango.build_serum_open_orders_watcher( context, websocket_manager, health_check, market, wallet) latest_bids_watcher: mango.Watcher[typing.Sequence[mango.Order]] = mango.build_serum_orderbook_side_watcher( @@ -127,7 +129,10 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man latest_asks_watcher: mango.Watcher[typing.Sequence[mango.Order]] = mango.build_serum_orderbook_side_watcher( context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.ASKS) elif isinstance(market, mango.SpotMarket): - inventory_watcher = mango.SpotInventoryAccountWatcher(market, latest_account_observer) + cache: mango.Cache = mango.Cache.load(context, group.cache) + cache_watcher: mango.Watcher[mango.Cache] = mango.build_cache_watcher( + context, websocket_manager, health_check, cache, group) + inventory_watcher = mango.SpotInventoryAccountWatcher(market, latest_account_observer, cache_watcher) latest_open_orders_observer = mango.build_spot_open_orders_watcher( context, websocket_manager, health_check, wallet, account, group, market) latest_bids_watcher = mango.build_serum_orderbook_side_watcher( @@ -135,7 +140,9 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man latest_asks_watcher = mango.build_serum_orderbook_side_watcher( context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.ASKS) elif isinstance(market, mango.PerpMarket): - inventory_watcher = mango.PerpInventoryAccountWatcher(market, latest_account_observer, group) + cache = mango.Cache.load(context, group.cache) + cache_watcher = mango.build_cache_watcher(context, websocket_manager, health_check, cache, group) + inventory_watcher = mango.PerpInventoryAccountWatcher(market, latest_account_observer, cache_watcher, group) latest_open_orders_observer = mango.build_perp_open_orders_watcher( context, websocket_manager, health_check, market, account, group, account_subscription) latest_bids_watcher = mango.build_perp_orderbook_side_watcher( diff --git a/mango/marketmaking/orderchain/confidenceintervalspreadelement.py b/mango/marketmaking/orderchain/confidenceintervalspreadelement.py index 7aa7deb..f55aa09 100644 --- a/mango/marketmaking/orderchain/confidenceintervalspreadelement.py +++ b/mango/marketmaking/orderchain/confidenceintervalspreadelement.py @@ -40,11 +40,7 @@ class ConfidenceIntervalSpreadElement(Element): if price.source.supports & mango.SupportedOracleFeature.CONFIDENCE == 0: raise Exception(f"Price does not support confidence interval: {price}") - base_tokens: mango.TokenValue = model_state.inventory.base - quote_tokens: mango.TokenValue = model_state.inventory.quote - - total = (base_tokens.value * price.mid_price) + quote_tokens.value - quote_value_to_risk = total * self.position_size_ratio + quote_value_to_risk = model_state.inventory.available_collateral.value * self.position_size_ratio position_size = quote_value_to_risk / price.mid_price new_orders: typing.List[mango.Order] = [] diff --git a/mango/watchers.py b/mango/watchers.py index 60d6661..e8f9c43 100644 --- a/mango/watchers.py +++ b/mango/watchers.py @@ -23,6 +23,7 @@ from solana.publickey import PublicKey from .account import Account from .accountinfo import AccountInfo +from .cache import Cache from .combinableinstructions import CombinableInstructions from .context import Context from .group import Group @@ -70,6 +71,16 @@ def build_account_watcher(context: Context, manager: WebSocketSubscriptionManage return account_subscription, latest_account_observer +def build_cache_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, cache: Cache, group: Group) -> Watcher[Cache]: + cache_subscription = WebSocketAccountSubscription[Cache]( + context, group.cache, lambda account_info: Cache.parse(account_info)) + manager.add(cache_subscription) + latest_cache_observer = LatestItemObserverSubscriber[Cache](cache) + cache_subscription.publisher.subscribe(latest_cache_observer) + health_check.add("cache_subscription", cache_subscription.publisher) + return latest_cache_observer + + def build_spot_open_orders_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, wallet: Wallet, account: Account, group: Group, spot_market: SpotMarket) -> Watcher[PlacedOrdersContainer]: market_index = group.find_spot_market_index(spot_market.address) open_orders_address = account.spot_open_orders[market_index] @@ -153,12 +164,11 @@ def build_price_watcher(context: Context, manager: WebSocketSubscriptionManager, return latest_price_observer -def build_serum_inventory_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, disposer: DisposePropagator, wallet: Wallet, market: Market) -> Watcher[Inventory]: +def build_serum_inventory_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, disposer: DisposePropagator, wallet: Wallet, market: Market, price_watcher: Watcher[Price]) -> Watcher[Inventory]: base_account = TokenAccount.fetch_largest_for_owner_and_token( context, wallet.address, market.base) if base_account is None: - raise Exception( - f"Could not find token account owned by {wallet.address} for base token {market.base}.") + raise Exception(f"Could not find token account owned by {wallet.address} for base token {market.base}.") base_token_subscription = WebSocketAccountSubscription[TokenAccount]( context, base_account.address, lambda account_info: TokenAccount.parse(account_info, market.base)) manager.add(base_token_subscription) @@ -169,8 +179,7 @@ def build_serum_inventory_watcher(context: Context, manager: WebSocketSubscripti quote_account = TokenAccount.fetch_largest_for_owner_and_token( context, wallet.address, market.quote) if quote_account is None: - raise Exception( - f"Could not find token account owned by {wallet.address} for quote token {market.quote}.") + raise Exception(f"Could not find token account owned by {wallet.address} for quote token {market.quote}.") quote_token_subscription = WebSocketAccountSubscription[TokenAccount]( context, quote_account.address, lambda account_info: TokenAccount.parse(account_info, market.quote)) manager.add(quote_token_subscription) @@ -185,7 +194,11 @@ def build_serum_inventory_watcher(context: Context, manager: WebSocketSubscripti mngo_accrued: TokenValue = TokenValue(mngo, Decimal(0)) def serum_inventory_accessor() -> Inventory: + available: Decimal = (latest_base_token_account_observer.latest.value.value * price_watcher.latest.mid_price) + \ + latest_quote_token_account_observer.latest.value.value + available_collateral: TokenValue = TokenValue(latest_quote_token_account_observer.latest.value.token, available) return Inventory(InventorySource.SPL_TOKENS, mngo_accrued, + available_collateral, latest_base_token_account_observer.latest.value, latest_quote_token_account_observer.latest.value)