Added CollateralCalculator to calculate collateral in different market types.
This commit is contained in:
parent
0f2596a4ce
commit
8bf6384076
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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)
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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] = []
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue