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 .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

View File

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

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
# 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":

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.
#
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:

View File

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

View File

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

View File

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

View File

@ -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] = []

View File

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