Added CollateralCalculator to calculate collateral in different market types.

This commit is contained in:
Geoff Taylor 2021-08-31 00:30:22 +01:00
parent 0f2596a4ce
commit 8bf6384076
10 changed files with 240 additions and 48 deletions

View File

@ -8,6 +8,7 @@ from .addressableaccount import AddressableAccount
from .balancesheet import BalanceSheet from .balancesheet import BalanceSheet
from .cache import PriceCache, RootBankCache, PerpMarketCache, Cache from .cache import PriceCache, RootBankCache, PerpMarketCache, Cache
from .client import CompatibleClient, BetterClient from .client import CompatibleClient, BetterClient
from .collateralcalculator import CollateralCalculator, SerumCollateralCalculator, SpotCollateralCalculator, PerpCollateralCalculator
from .combinableinstructions import CombinableInstructions from .combinableinstructions import CombinableInstructions
from .constants import SYSTEM_PROGRAM_ADDRESS, SOL_MINT_ADDRESS, SOL_DECIMALS, SOL_DECIMAL_DIVISOR, WARNING_DISCLAIMER_TEXT, MangoConstants from .constants import SYSTEM_PROGRAM_ADDRESS, SOL_MINT_ADDRESS, SOL_DECIMALS, SOL_DECIMAL_DIVISOR, WARNING_DISCLAIMER_TEXT, MangoConstants
from .context import Context from .context import Context
@ -77,7 +78,7 @@ from .version import Version
from .wallet import Wallet from .wallet import Wallet
from .walletbalancer import TargetBalance, FixedTargetBalance, PercentageTargetBalance, TargetBalanceParser, sort_changes_for_trades, calculate_required_balance_changes, FilterSmallChanges, WalletBalancer, NullWalletBalancer, LiveWalletBalancer 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 .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 .websocketsubscription import WebSocketSubscription, WebSocketProgramSubscription, WebSocketAccountSubscription, WebSocketLogSubscription, WebSocketSubscriptionManager, IndividualWebSocketSubscriptionManager, SharedWebSocketSubscriptionManager
from .layouts import layouts from .layouts import layouts

View File

@ -13,9 +13,6 @@
# [Github](https://github.com/blockworks-foundation) # [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation) # [Email](mailto:hello@blockworks.foundation)
from mango.perpopenorders import PerpOpenOrders
from mango.placedorder import PlacedOrder
from mango.tokeninfo import TokenInfo
import typing import typing
from decimal import Decimal from decimal import Decimal
@ -31,7 +28,10 @@ from .layouts import layouts
from .metadata import Metadata from .metadata import Metadata
from .orders import Side from .orders import Side
from .perpaccount import PerpAccount from .perpaccount import PerpAccount
from .perpopenorders import PerpOpenOrders
from .placedorder import PlacedOrder
from .token import Token from .token import Token
from .tokeninfo import TokenInfo
from .tokenvalue import TokenValue from .tokenvalue import TokenValue
from .version import Version from .version import Version

View File

@ -37,7 +37,6 @@ from .serumeventqueue import SerumEventQueue
# Given a `Context` and an account type, returns a function that can take an `AccountInfo` and # Given a `Context` and an account type, returns a function that can take an `AccountInfo` and
# return one of our objects. # return one of our objects.
# #
def build_account_info_converter(context: Context, account_type: str) -> typing.Callable[[AccountInfo], AddressableAccount]: def build_account_info_converter(context: Context, account_type: str) -> typing.Callable[[AccountInfo], AddressableAccount]:
account_type_upper = account_type.upper() account_type_upper = account_type.upper()
if account_type_upper == "GROUP": if account_type_upper == "GROUP":

View File

@ -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)

View File

@ -74,7 +74,6 @@ TMappedGroupBasketValue = typing.TypeVar("TMappedGroupBasketValue")
# #
# `Group` defines root functionality for Mango Markets. # `Group` defines root functionality for Mango Markets.
# #
class Group(AddressableAccount): class Group(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version, name: str, def __init__(self, account_info: AccountInfo, version: Version, name: str,
meta_data: Metadata, meta_data: Metadata,
@ -122,6 +121,10 @@ class Group(AddressableAccount):
def perp_markets(self) -> typing.Sequence[typing.Optional[PerpMarketInfo]]: 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) 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 @staticmethod
def from_layout(context: Context, layout: typing.Any, name: str, account_info: AccountInfo, version: Version, token_lookup: TokenLookup, market_lookup: MarketLookup) -> "Group": 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) 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}") 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: def find_token_info_by_token(self, token: Token) -> TokenInfo:
for token_info in self.tokens: for token_info in self.tokens:
if token_info is not None and token_info.token == token: if token_info is not None and token_info.token == token:

View File

@ -19,6 +19,8 @@ import logging
from decimal import Decimal from decimal import Decimal
from .account import Account from .account import Account
from .cache import Cache
from .collateralcalculator import CollateralCalculator, SpotCollateralCalculator, PerpCollateralCalculator
from .group import Group from .group import Group
from .market import InventorySource, Market from .market import InventorySource, Market
from .perpmarket import PerpMarket from .perpmarket import PerpMarket
@ -30,11 +32,11 @@ from .watcher import Watcher
# #
# This class details inventory of a crypto account for a market. # This class details inventory of a crypto account for a market.
# #
class Inventory: 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.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.inventory_source: InventorySource = inventory_source self.inventory_source: InventorySource = inventory_source
self.available_collateral: TokenValue = available_collateral
self.liquidity_incentives: TokenValue = liquidity_incentives self.liquidity_incentives: TokenValue = liquidity_incentives
self.base: TokenValue = base self.base: TokenValue = base
self.quote: TokenValue = quote self.quote: TokenValue = quote
@ -47,29 +49,34 @@ class Inventory:
liquidity_incentives: str = "" liquidity_incentives: str = ""
if self.liquidity_incentives.value > 0: if self.liquidity_incentives.value > 0:
liquidity_incentives = f" {self.liquidity_incentives}" 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: def __repr__(self) -> str:
return f"{self}" return f"{self}"
class SpotInventoryAccountWatcher: 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.account_watcher: Watcher[Account] = account_watcher
self.cache_watcher: Watcher[Cache] = cache_watcher
account: Account = account_watcher.latest account: Account = account_watcher.latest
base_value = TokenValue.find_by_symbol(account.net_assets, market.base.symbol) base_value = TokenValue.find_by_symbol(account.net_assets, market.base.symbol)
self.base_index: int = account.net_assets.index(base_value) self.base_index: int = account.net_assets.index(base_value)
quote_value = TokenValue.find_by_symbol(account.net_assets, market.quote.symbol) quote_value = TokenValue.find_by_symbol(account.net_assets, market.quote.symbol)
self.quote_index: int = account.net_assets.index(quote_value) self.quote_index: int = account.net_assets.index(quote_value)
self.collateral_calculator: CollateralCalculator = SpotCollateralCalculator()
@property @property
def latest(self) -> Inventory: def latest(self) -> Inventory:
account: Account = self.account_watcher.latest account: Account = self.account_watcher.latest
cache: Cache = self.cache_watcher.latest
# Spot markets don't accrue MNGO liquidity incentives # Spot markets don't accrue MNGO liquidity incentives
mngo = account.group.find_token_info_by_symbol("MNGO").token mngo = account.group.find_token_info_by_symbol("MNGO").token
mngo_accrued: TokenValue = TokenValue(mngo, Decimal(0)) mngo_accrued: TokenValue = TokenValue(mngo, Decimal(0))
available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache)
base_value = account.net_assets[self.base_index] base_value = account.net_assets[self.base_index]
if base_value is None: if base_value is None:
raise Exception( raise Exception(
@ -79,29 +86,35 @@ class SpotInventoryAccountWatcher:
raise Exception( raise Exception(
f"Could not find net assets in account {account.address} at index {self.quote_index}.") 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: 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.market: PerpMarket = market
self.account_watcher: Watcher[Account] = account_watcher 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) self.perp_account_index: int = group.find_perp_market_index(market.address)
account: Account = account_watcher.latest account: Account = account_watcher.latest
quote_value = TokenValue.find_by_symbol(account.net_assets, market.quote.symbol) quote_value = TokenValue.find_by_symbol(account.net_assets, market.quote.symbol)
self.quote_index: int = account.net_assets.index(quote_value) self.quote_index: int = account.net_assets.index(quote_value)
self.collateral_calculator: CollateralCalculator = PerpCollateralCalculator()
@property @property
def latest(self) -> Inventory: 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: if perp_account is None:
raise Exception( 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_lots = perp_account.base_position
base_value = self.market.lot_size_converter.quantity_lots_to_value(base_lots) base_value = self.market.lot_size_converter.quantity_lots_to_value(base_lots)
base_token_value = TokenValue(self.market.base, base_value) 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: if quote_token_value is None:
raise Exception( raise Exception(f"Could not find net assets in account {account.address} at index {self.quote_index}.")
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, available_collateral, base_token_value, quote_token_value)
return Inventory(InventorySource.ACCOUNT, perp_account.mngo_accrued, base_token_value, quote_token_value)

View File

@ -147,10 +147,6 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
account_infos[3], self.base_inventory_token_account.value.token) account_infos[3], self.base_inventory_token_account.value.token)
quote_inventory_token_account = mango.TokenAccount.parse( quote_inventory_token_account = mango.TokenAccount.parse(
account_infos[4], self.quote_inventory_token_account.value.token) 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( bids: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders(
account_infos[5], self.market.underlying_serum_market) account_infos[5], self.market.underlying_serum_market)
@ -159,6 +155,15 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
price: mango.Price = self.oracle.fetch_price(context) 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) return self.from_values(self.market, group, account, price, placed_orders_container, inventory, bids, asks)
def __str__(self) -> str: def __str__(self) -> str:
@ -174,6 +179,7 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
market: mango.SpotMarket, market: mango.SpotMarket,
oracle: mango.Oracle, oracle: mango.Oracle,
group_address: PublicKey, group_address: PublicKey,
cache_address: PublicKey,
account_address: PublicKey, account_address: PublicKey,
open_orders_address: PublicKey open_orders_address: PublicKey
): ):
@ -182,12 +188,16 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
self.oracle: mango.Oracle = oracle self.oracle: mango.Oracle = oracle
self.group_address: PublicKey = group_address self.group_address: PublicKey = group_address
self.cache_address: PublicKey = cache_address
self.account_address: PublicKey = account_address self.account_address: PublicKey = account_address
self.open_orders_address: PublicKey = open_orders_address self.open_orders_address: PublicKey = open_orders_address
self.collateral_calculator: mango.CollateralCalculator = mango.SpotCollateralCalculator()
def poll(self, context: mango.Context) -> ModelState: def poll(self, context: mango.Context) -> ModelState:
addresses: typing.List[PublicKey] = [ addresses: typing.List[PublicKey] = [
self.group_address, self.group_address,
self.cache_address,
self.account_address, self.account_address,
self.open_orders_address, self.open_orders_address,
self.market.underlying_serum_market.state.bids(), 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) account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses)
group: mango.Group = mango.Group.parse(context, account_infos[0]) 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( 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 # Spot markets don't accrue MNGO liquidity incentives
mngo = account.group.find_token_info_by_symbol("MNGO").token 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) 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) 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( 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) 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) price: mango.Price = self.oracle.fetch_price(context)
@ -230,6 +246,7 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder):
market: mango.PerpMarket, market: mango.PerpMarket,
oracle: mango.Oracle, oracle: mango.Oracle,
group_address: PublicKey, group_address: PublicKey,
cache_address: PublicKey,
account_address: PublicKey account_address: PublicKey
): ):
super().__init__() super().__init__()
@ -237,18 +254,23 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder):
self.oracle: mango.Oracle = oracle self.oracle: mango.Oracle = oracle
self.group_address: PublicKey = group_address self.group_address: PublicKey = group_address
self.cache_address: PublicKey = cache_address
self.account_address: PublicKey = account_address self.account_address: PublicKey = account_address
self.collateral_calculator: mango.CollateralCalculator = mango.PerpCollateralCalculator()
def poll(self, context: mango.Context) -> ModelState: def poll(self, context: mango.Context) -> ModelState:
addresses: typing.List[PublicKey] = [ addresses: typing.List[PublicKey] = [
self.group_address, self.group_address,
self.cache_address,
self.account_address, self.account_address,
self.market.underlying_perp_market.bids, self.market.underlying_perp_market.bids,
self.market.underlying_perp_market.asks self.market.underlying_perp_market.asks
] ]
account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses) account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses)
group: mango.Group = mango.Group.parse(context, account_infos[0]) 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) index = group.find_perp_market_index(self.market.address)
perp_account = account.perp_accounts[index] 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_value = self.market.lot_size_converter.quantity_lots_to_value(base_lots)
base_token_value = mango.TokenValue(self.market.base, base_value) 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) quote_token_value = mango.TokenValue.find_by_symbol(account.net_assets, self.market.quote.symbol)
inventory: mango.Inventory = mango.Inventory( available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache)
mango.InventorySource.ACCOUNT, perp_account.mngo_accrued, base_token_value, quote_token_value) 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( 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) 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) price: mango.Price = self.oracle.fetch_price(context)

View File

@ -92,12 +92,12 @@ def _polling_spot_model_state_builder_factory(group: mango.Group, account: mango
raise Exception( raise Exception(
f"Could not find spot openorders in account {account.address} for market {market.symbol}.") f"Could not find spot openorders in account {account.address} for market {market.symbol}.")
return SpotPollingModelStateBuilder( 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, def _polling_perp_model_state_builder_factory(group: mango.Group, account: mango.Account, market: mango.PerpMarket,
oracle: mango.Oracle) -> ModelStateBuilder: 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, 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) market = mango.ensure_market_loaded(context, market)
if isinstance(market, mango.SerumMarket): 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( 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( latest_open_orders_observer: mango.Watcher[mango.PlacedOrdersContainer] = mango.build_serum_open_orders_watcher(
context, websocket_manager, health_check, market, wallet) context, websocket_manager, health_check, market, wallet)
latest_bids_watcher: mango.Watcher[typing.Sequence[mango.Order]] = mango.build_serum_orderbook_side_watcher( 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( 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) context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.ASKS)
elif isinstance(market, mango.SpotMarket): 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( latest_open_orders_observer = mango.build_spot_open_orders_watcher(
context, websocket_manager, health_check, wallet, account, group, market) context, websocket_manager, health_check, wallet, account, group, market)
latest_bids_watcher = mango.build_serum_orderbook_side_watcher( 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( latest_asks_watcher = mango.build_serum_orderbook_side_watcher(
context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.ASKS) context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.ASKS)
elif isinstance(market, mango.PerpMarket): 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( latest_open_orders_observer = mango.build_perp_open_orders_watcher(
context, websocket_manager, health_check, market, account, group, account_subscription) context, websocket_manager, health_check, market, account, group, account_subscription)
latest_bids_watcher = mango.build_perp_orderbook_side_watcher( latest_bids_watcher = mango.build_perp_orderbook_side_watcher(

View File

@ -40,11 +40,7 @@ class ConfidenceIntervalSpreadElement(Element):
if price.source.supports & mango.SupportedOracleFeature.CONFIDENCE == 0: if price.source.supports & mango.SupportedOracleFeature.CONFIDENCE == 0:
raise Exception(f"Price does not support confidence interval: {price}") raise Exception(f"Price does not support confidence interval: {price}")
base_tokens: mango.TokenValue = model_state.inventory.base quote_value_to_risk = model_state.inventory.available_collateral.value * self.position_size_ratio
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
position_size = quote_value_to_risk / price.mid_price position_size = quote_value_to_risk / price.mid_price
new_orders: typing.List[mango.Order] = [] new_orders: typing.List[mango.Order] = []

View File

@ -23,6 +23,7 @@ from solana.publickey import PublicKey
from .account import Account from .account import Account
from .accountinfo import AccountInfo from .accountinfo import AccountInfo
from .cache import Cache
from .combinableinstructions import CombinableInstructions from .combinableinstructions import CombinableInstructions
from .context import Context from .context import Context
from .group import Group from .group import Group
@ -70,6 +71,16 @@ def build_account_watcher(context: Context, manager: WebSocketSubscriptionManage
return account_subscription, latest_account_observer 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]: 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) market_index = group.find_spot_market_index(spot_market.address)
open_orders_address = account.spot_open_orders[market_index] 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 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( base_account = TokenAccount.fetch_largest_for_owner_and_token(
context, wallet.address, market.base) context, wallet.address, market.base)
if base_account is None: if base_account is None:
raise Exception( raise Exception(f"Could not find token account owned by {wallet.address} for base token {market.base}.")
f"Could not find token account owned by {wallet.address} for base token {market.base}.")
base_token_subscription = WebSocketAccountSubscription[TokenAccount]( base_token_subscription = WebSocketAccountSubscription[TokenAccount](
context, base_account.address, lambda account_info: TokenAccount.parse(account_info, market.base)) context, base_account.address, lambda account_info: TokenAccount.parse(account_info, market.base))
manager.add(base_token_subscription) 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( quote_account = TokenAccount.fetch_largest_for_owner_and_token(
context, wallet.address, market.quote) context, wallet.address, market.quote)
if quote_account is None: if quote_account is None:
raise Exception( raise Exception(f"Could not find token account owned by {wallet.address} for quote token {market.quote}.")
f"Could not find token account owned by {wallet.address} for quote token {market.quote}.")
quote_token_subscription = WebSocketAccountSubscription[TokenAccount]( quote_token_subscription = WebSocketAccountSubscription[TokenAccount](
context, quote_account.address, lambda account_info: TokenAccount.parse(account_info, market.quote)) context, quote_account.address, lambda account_info: TokenAccount.parse(account_info, market.quote))
manager.add(quote_token_subscription) 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)) mngo_accrued: TokenValue = TokenValue(mngo, Decimal(0))
def serum_inventory_accessor() -> Inventory: 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, return Inventory(InventorySource.SPL_TOKENS, mngo_accrued,
available_collateral,
latest_base_token_account_observer.latest.value, latest_base_token_account_observer.latest.value,
latest_quote_token_account_observer.latest.value) latest_quote_token_account_observer.latest.value)