diff --git a/bin/show-account-balances b/bin/show-account-balances index fc3ce9a..311b59c 100755 --- a/bin/show-account-balances +++ b/bin/show-account-balances @@ -71,10 +71,9 @@ for account in mango_accounts: report: mango.AccountTokenValues = mango.AccountTokenValues.from_account_basket_base_token( asset, open_orders, group) # print(report) + market_cache: mango.MarketCache = group.market_cache_from_cache(cache, report.base_token) price_from_cache: mango.TokenValue = group.token_price_from_cache(cache, report.base_token) - perp_market_cache: typing.Optional[mango.PerpMarketCache] = group.perp_market_cache_from_cache( - cache, report.base_token) - priced_report: mango.AccountTokenValues = report.priced(price_from_cache, perp_market_cache) + priced_report: mango.AccountTokenValues = report.priced(market_cache) account_value += priced_report.net_value quote_token_free_in_open_orders += priced_report.quote_token_free quote_token_total_in_open_orders += priced_report.quote_token_total @@ -82,7 +81,9 @@ for account in mango_accounts: quote_report: mango.AccountTokenValues = mango.AccountTokenValues(account.shared_quote_token.token_info.token, account.shared_quote_token.token_info.token, + account.shared_quote_token.raw_deposit, account.shared_quote_token.deposit, + account.shared_quote_token.raw_borrow, account.shared_quote_token.borrow, mango.TokenValue( group.shared_quote_token.token, Decimal(0)), @@ -92,7 +93,12 @@ for account in mango_accounts: quote_token_total_in_open_orders, mango.TokenValue( group.shared_quote_token.token, Decimal(0)), - Decimal(0), Decimal(0), Decimal(0), + Decimal(0), Decimal(0), + mango.TokenValue( + group.shared_quote_token.token, Decimal(0)), + mango.TokenValue( + group.shared_quote_token.token, Decimal(0)), + Decimal(0), Decimal(0), mango.NullLotSizeConverter()) account_value += quote_report.net_value + quote_token_total_in_open_orders print(quote_report) diff --git a/mango/__init__.py b/mango/__init__.py index 2266c8a..d64ef89 100644 --- a/mango/__init__.py +++ b/mango/__init__.py @@ -8,7 +8,7 @@ from .accounttokenvalues import AccountTokenValues, PricedAccountTokenValues from .addressableaccount import AddressableAccount from .arguments import parse_args from .balancesheet import BalanceSheet -from .cache import PriceCache, RootBankCache, PerpMarketCache, Cache +from .cache import PriceCache, RootBankCache, PerpMarketCache, MarketCache, Cache from .client import ClientException, RateLimitException, TooMuchBandwidthRateLimitException, TooManyRequestsRateLimitException, BlockhashNotFoundException, NodeIsBehindException, FailedToFetchBlockhashException, TransactionException, CompatibleClient, BetterClient from .combinableinstructions import CombinableInstructions from .constants import SYSTEM_PROGRAM_ADDRESS, SOL_MINT_ADDRESS, SOL_DECIMALS, SOL_DECIMAL_DIVISOR, WARNING_DISCLAIMER_TEXT, MangoConstants diff --git a/mango/account.py b/mango/account.py index 7d51ed4..2480edd 100644 --- a/mango/account.py +++ b/mango/account.py @@ -42,20 +42,26 @@ from .version import Version # `AccountBasketToken` gathers basket items together instead of separate arrays. # class AccountBasketToken: - def __init__(self, token_info: TokenInfo, deposit: TokenValue, borrow: TokenValue): + def __init__(self, token_info: TokenInfo, raw_deposit: Decimal, deposit: TokenValue, raw_borrow: Decimal, borrow: TokenValue): self.token_info: TokenInfo = token_info + self.raw_deposit: Decimal = raw_deposit self.deposit: TokenValue = deposit + self.raw_borrow: Decimal = raw_borrow self.borrow: TokenValue = borrow @property def net_value(self) -> TokenValue: return self.deposit - self.borrow + @property + def raw_net_value(self) -> Decimal: + return self.raw_deposit - self.raw_borrow + def __str__(self) -> str: return f"""Β« π™°πšŒπšŒπš˜πšžπš—πšπ™±πšŠπšœπš”πšŽπšπšƒπš˜πš”πšŽπš— {self.token_info.token.symbol} Net Value: {self.net_value} - Deposited: {self.deposit} - Borrowed: {self.borrow} + Deposited: {self.deposit} (raw value: {self.raw_deposit}) + Borrowed: {self.borrow} (raw value {self.raw_borrow}) Β»""" def __repr__(self) -> str: @@ -68,8 +74,8 @@ class AccountBasketToken: # account. # class AccountBasketBaseToken(AccountBasketToken): - def __init__(self, token_info: TokenInfo, quote_token_info: TokenInfo, deposit: TokenValue, borrow: TokenValue, spot_open_orders: typing.Optional[PublicKey], perp_account: typing.Optional[PerpAccount]): - super().__init__(token_info, deposit, borrow) + def __init__(self, token_info: TokenInfo, quote_token_info: TokenInfo, raw_deposit: Decimal, deposit: TokenValue, raw_borrow: Decimal, borrow: TokenValue, spot_open_orders: typing.Optional[PublicKey], perp_account: typing.Optional[PerpAccount]): + super().__init__(token_info, raw_deposit, deposit, raw_borrow, borrow) self.quote_token_info: TokenInfo = quote_token_info self.spot_open_orders: typing.Optional[PublicKey] = spot_open_orders self.perp_account: typing.Optional[PerpAccount] = perp_account @@ -80,8 +86,8 @@ class AccountBasketBaseToken(AccountBasketToken): perp_account = f"{self.perp_account}".replace("\n", "\n ") return f"""Β« π™°πšŒπšŒπš˜πšžπš—πšπ™±πšŠπšœπš”πšŽπšπ™±πšŠπšœπšŽπšƒπš˜πš”πšŽπš— {self.token_info.token.symbol} Net Value: {self.net_value} - Deposited: {self.deposit} - Borrowed: {self.borrow} + Deposited: {self.deposit} (raw value: {self.raw_deposit}) + Borrowed: {self.borrow} (raw value {self.raw_borrow}) Spot OpenOrders: {self.spot_open_orders or "None"} Perp Account: {perp_account} @@ -146,9 +152,11 @@ class Account(AddressableAccount): for index, token_info in enumerate(group.tokens[:-1]): if token_info: - intrinsic_deposit = token_info.root_bank.deposit_index * layout.deposits[index] + raw_deposit: Decimal = layout.deposits[index] + intrinsic_deposit = token_info.root_bank.deposit_index * raw_deposit deposit = TokenValue(token_info.token, token_info.token.shift_to_decimals(intrinsic_deposit)) - intrinsic_borrow = token_info.root_bank.borrow_index * layout.borrows[index] + raw_borrow: Decimal = layout.borrows[index] + intrinsic_borrow = token_info.root_bank.borrow_index * raw_borrow borrow = TokenValue(token_info.token, token_info.token.shift_to_decimals(intrinsic_borrow)) perp_open_orders = PerpOpenOrders(placed_orders_all_markets[index]) group_basket_market = group.markets[index] @@ -163,19 +171,22 @@ class Account(AddressableAccount): mngo_token_info.token) spot_open_orders = layout.spot_open_orders[index] basket_item: AccountBasketBaseToken = AccountBasketBaseToken( - token_info, quote_token_info, deposit, borrow, spot_open_orders, perp_account) + token_info, quote_token_info, raw_deposit, deposit, raw_borrow, borrow, spot_open_orders, perp_account) basket += [basket_item] active_in_basket += [True] else: active_in_basket += [False] - intrinsic_quote_deposit = quote_token_info.root_bank.deposit_index * layout.deposits[-1] + raw_quote_deposit: Decimal = layout.deposits[-1] + intrinsic_quote_deposit = quote_token_info.root_bank.deposit_index * raw_quote_deposit quote_deposit = TokenValue(quote_token_info.token, quote_token_info.token.shift_to_decimals(intrinsic_quote_deposit)) - intrinsic_quote_borrow = quote_token_info.root_bank.borrow_index * layout.borrows[-1] + raw_quote_borrow: Decimal = layout.borrows[-1] + intrinsic_quote_borrow = quote_token_info.root_bank.borrow_index * raw_quote_borrow quote_borrow = TokenValue(quote_token_info.token, quote_token_info.token.shift_to_decimals(intrinsic_quote_borrow)) - quote: AccountBasketToken = AccountBasketToken(quote_token_info, quote_deposit, quote_borrow) + quote: AccountBasketToken = AccountBasketToken( + quote_token_info, raw_quote_deposit, quote_deposit, raw_quote_borrow, quote_borrow) msrm_amount: Decimal = layout.msrm_amount being_liquidated: bool = bool(layout.being_liquidated) diff --git a/mango/accounttokenvalues.py b/mango/accounttokenvalues.py index b2830a0..b0e91d4 100644 --- a/mango/accounttokenvalues.py +++ b/mango/accounttokenvalues.py @@ -18,7 +18,7 @@ import typing from decimal import Decimal from .account import AccountBasketBaseToken -from .cache import PerpMarketCache +from .cache import PerpMarketCache, MarketCache from .calculators.unsettledfundingcalculator import calculate_unsettled_funding, UnsettledFundingParams from .group import Group from .lotsizeconverter import LotSizeConverter @@ -55,10 +55,12 @@ def _token_values_from_open_orders(base_token: Token, quote_token: Token, spot_o # `AccountTokenValues` gathers basket items together instead of separate arrays. # class AccountTokenValues: - def __init__(self, base_token: Token, quote_token: Token, deposit: TokenValue, borrow: TokenValue, base_token_free: TokenValue, base_token_total: TokenValue, quote_token_free: TokenValue, quote_token_total: TokenValue, perp_base_position: TokenValue, raw_perp_quote_position: Decimal, long_settled_funding: Decimal, short_settled_funding: Decimal, lot_size_converter: LotSizeConverter): + def __init__(self, base_token: Token, quote_token: Token, raw_deposit: Decimal, deposit: TokenValue, raw_borrow: Decimal, borrow: TokenValue, base_token_free: TokenValue, base_token_total: TokenValue, quote_token_free: TokenValue, quote_token_total: TokenValue, perp_base_position: TokenValue, raw_perp_quote_position: Decimal, raw_taker_quote: Decimal, bids_quantity: TokenValue, asks_quantity: TokenValue, long_settled_funding: Decimal, short_settled_funding: Decimal, lot_size_converter: LotSizeConverter): self.base_token: Token = base_token self.quote_token: Token = quote_token + self.raw_deposit: Decimal = raw_deposit self.deposit: TokenValue = deposit + self.raw_borrow: Decimal = raw_borrow self.borrow: TokenValue = borrow self.base_token_free: TokenValue = base_token_free @@ -67,6 +69,9 @@ class AccountTokenValues: self.quote_token_total: TokenValue = quote_token_total self.perp_base_position: TokenValue = perp_base_position self.raw_perp_quote_position: Decimal = raw_perp_quote_position + self.raw_taker_quote: Decimal = raw_taker_quote + self.bids_quantity: TokenValue = bids_quantity + self.asks_quantity: TokenValue = asks_quantity self.long_settled_funding: Decimal = long_settled_funding self.short_settled_funding: Decimal = short_settled_funding self.lot_size_converter: LotSizeConverter = lot_size_converter @@ -83,8 +88,16 @@ class AccountTokenValues: def quote_token_locked(self) -> TokenValue: return self.quote_token_total - self.quote_token_free - def priced(self, price: TokenValue, perp_market_cache: typing.Optional[PerpMarketCache]) -> "PricedAccountTokenValues": - return PricedAccountTokenValues(self, price, perp_market_cache) + @property + def if_all_bids_executed(self) -> TokenValue: + return self.perp_base_position + self.bids_quantity + + @property + def if_all_asks_executed(self) -> TokenValue: + return self.perp_base_position - self.asks_quantity + + def priced(self, market_cache: MarketCache) -> "PricedAccountTokenValues": + return PricedAccountTokenValues(self, market_cache) @staticmethod def from_account_basket_base_token(account_basket_token: AccountBasketBaseToken, open_orders_by_address: typing.Dict[str, OpenOrders], group: Group) -> "AccountTokenValues": @@ -106,16 +119,28 @@ class AccountTokenValues: long_settled_funding: Decimal = perp_account.long_settled_funding / lot_size_converter.quote_lot_size short_settled_funding: Decimal = perp_account.short_settled_funding / lot_size_converter.quote_lot_size - return AccountTokenValues(base_token, quote_token, account_basket_token.deposit, account_basket_token.borrow, base_token_free, base_token_total, quote_token_free, quote_token_total, perp_base_position, perp_quote_position, long_settled_funding, short_settled_funding, lot_size_converter) + taker_quote: Decimal = perp_account.taker_quote * lot_size_converter.quote_lot_size + bids_quantity: TokenValue = TokenValue(base_token, base_token.shift_to_decimals( + perp_account.bids_quantity * lot_size_converter.base_lot_size)) + asks_quantity: TokenValue = TokenValue(base_token, base_token.shift_to_decimals( + perp_account.asks_quantity * lot_size_converter.base_lot_size)) + + return AccountTokenValues(base_token, quote_token, account_basket_token.raw_deposit, account_basket_token.deposit, account_basket_token.raw_borrow, account_basket_token.borrow, base_token_free, base_token_total, quote_token_free, quote_token_total, perp_base_position, perp_quote_position, taker_quote, bids_quantity, asks_quantity, long_settled_funding, short_settled_funding, lot_size_converter) def __str__(self) -> str: return f"""Β« π™°πšŒπšŒπš˜πšžπš—πšπšƒπš˜πš”πšŽπš—πšπšŽπš™πš˜πš›πš {self.base_token.symbol} - Deposited: {self.deposit} - Borrowed : {self.borrow} + Deposited : {self.deposit} + Borrowed : {self.borrow} Unsettled: - Base : {self.base_token_total} ({self.base_token_free} free) - Quote : {self.quote_token_total} ({self.quote_token_free} free) - Net Value: {self.net_value} + Base : {self.base_token_total} ({self.base_token_free} free) + Quote : {self.quote_token_total} ({self.quote_token_free} free) + Perp: + Base : {self.perp_base_position} + Quote : {self.raw_perp_quote_position} + If Executed: + All Bids : {self.if_all_bids_executed} + All Asks : {self.if_all_asks_executed} + Net Value : {self.net_value} Β»""" def __repr__(self) -> str: @@ -123,29 +148,45 @@ class AccountTokenValues: class PricedAccountTokenValues(AccountTokenValues): - def __init__(self, original_account_token_values: AccountTokenValues, price: TokenValue, perp_market_cache: typing.Optional[PerpMarketCache]): - deposit: TokenValue = original_account_token_values.deposit * price - borrow: TokenValue = original_account_token_values.borrow * price + def __init__(self, original_account_token_values: AccountTokenValues, market_cache: MarketCache): + price: TokenValue = market_cache.adjusted_price( + original_account_token_values.base_token, original_account_token_values.quote_token) + + if market_cache.root_bank is None: + raise Exception(f"No root bank for token {original_account_token_values.base_token} in {market_cache}") + + deposit_value: Decimal = original_account_token_values.raw_deposit * market_cache.root_bank.deposit_index * price.value + shifted_deposit_value: Decimal = original_account_token_values.quote_token.shift_to_decimals(deposit_value) + deposit: TokenValue = TokenValue(original_account_token_values.quote_token, shifted_deposit_value) + + borrow_value: Decimal = original_account_token_values.raw_borrow * market_cache.root_bank.borrow_index * price.value + shifted_borrow_value: Decimal = original_account_token_values.quote_token.shift_to_decimals(borrow_value) + borrow: TokenValue = TokenValue(original_account_token_values.quote_token, shifted_borrow_value) + base_token_free: TokenValue = original_account_token_values.base_token_free * price base_token_total: TokenValue = original_account_token_values.base_token_total * price perp_base_position: TokenValue = original_account_token_values.perp_base_position * price super().__init__(original_account_token_values.base_token, original_account_token_values.quote_token, - deposit, borrow, base_token_free, base_token_total, + original_account_token_values.raw_deposit, deposit, + original_account_token_values.raw_borrow, borrow, base_token_free, base_token_total, original_account_token_values.quote_token_free, original_account_token_values.quote_token_total, perp_base_position, original_account_token_values.raw_perp_quote_position, + original_account_token_values.raw_taker_quote, + original_account_token_values.bids_quantity, original_account_token_values.asks_quantity, original_account_token_values.long_settled_funding, original_account_token_values.short_settled_funding, original_account_token_values.lot_size_converter) self.original_account_token_values: AccountTokenValues = original_account_token_values self.price: TokenValue = price - self.perp_market_cache: typing.Optional[PerpMarketCache] = perp_market_cache - perp_quote_position: TokenValue = TokenValue(original_account_token_values.quote_token, Decimal(0)) - if perp_market_cache is not None: + self.perp_market_cache: typing.Optional[PerpMarketCache] = market_cache.perp_market + perp_quote_position: TokenValue = TokenValue( + original_account_token_values.quote_token, original_account_token_values.raw_perp_quote_position) + if market_cache.perp_market is not None: original: AccountTokenValues = original_account_token_values - long_funding: Decimal = perp_market_cache.long_funding / original.lot_size_converter.quote_lot_size - short_funding: Decimal = perp_market_cache.short_funding / original.lot_size_converter.quote_lot_size + long_funding: Decimal = market_cache.perp_market.long_funding / original.lot_size_converter.quote_lot_size + short_funding: Decimal = market_cache.perp_market.short_funding / original.lot_size_converter.quote_lot_size unsettled_funding: TokenValue = calculate_unsettled_funding(UnsettledFundingParams( quote_token=original.quote_token, base_position=original.perp_base_position, @@ -154,21 +195,45 @@ class PricedAccountTokenValues(AccountTokenValues): short_funding=short_funding, short_settled_funding=original.short_settled_funding )) - perp_quote_position += unsettled_funding + perp_quote_position -= unsettled_funding self.perp_quote_position: TokenValue = perp_quote_position + @property + def if_all_bids_executed(self) -> TokenValue: + return self.perp_base_position + (self.bids_quantity * self.price) + + @property + def if_all_asks_executed(self) -> TokenValue: + return self.perp_base_position - (self.asks_quantity * self.price) + + def if_worst_execution(self) -> typing.Tuple[TokenValue, TokenValue]: + taker_quote: TokenValue = TokenValue(self.perp_quote_position.token, self.raw_taker_quote) + # print("Quote calc", self.perp_quote_position, taker_quote, self.bids_quantity, self.price) + + if abs(self.if_all_bids_executed.value) > abs(self.if_all_asks_executed.value): + base_position = self.if_all_bids_executed + quote_position = self.perp_quote_position + taker_quote - (self.bids_quantity * self.price) + else: + base_position = self.if_all_asks_executed + quote_position = self.perp_quote_position + taker_quote + (self.asks_quantity * self.price) + + return base_position, quote_position + def __str__(self) -> str: return f"""Β« π™Ώπš›πš’πšŒπšŽπšπ™°πšŒπšŒπš˜πšžπš—πšπšƒπš˜πš”πšŽπš—πš…πšŠπš•πšžπšŽπšœ {self.base_token.symbol} priced in {self.quote_token.symbol} - Deposited: {self.original_account_token_values.deposit:<45} worth {self.deposit} - Borrowed: {self.original_account_token_values.borrow:<45} worth {self.borrow} + Deposited : {self.original_account_token_values.deposit:<45} worth {self.deposit} + Borrowed : {self.original_account_token_values.borrow:<45} worth {self.borrow} Unsettled: - Base : {self.original_account_token_values.base_token_total:<45} worth {self.base_token_total} - Quote : {self.original_account_token_values.quote_token_total:<45} worth {self.quote_token_total} + Base : {self.original_account_token_values.base_token_total:<45} worth {self.base_token_total} + Quote : {self.original_account_token_values.quote_token_total:<45} worth {self.quote_token_total} Perp: - Base : {self.original_account_token_values.perp_base_position:<45} worth {self.perp_base_position} - Quote : {self.perp_quote_position:<45} worth {self.perp_quote_position} - Net Value: {self.original_account_token_values.net_value:<45} worth {self.net_value} + Base : {self.original_account_token_values.perp_base_position:<45} worth {self.perp_base_position} + Quote : {self.perp_quote_position:<45} worth {self.perp_quote_position} + If Executed: + All Bids : {self.original_account_token_values.if_all_bids_executed:<45} worth {self.if_all_bids_executed} + All Asks : {self.original_account_token_values.if_all_asks_executed:<45} worth {self.if_all_asks_executed} + Net Value : {self.original_account_token_values.net_value:<45} worth {self.net_value} Β»""" def __repr__(self) -> str: diff --git a/mango/cache.py b/mango/cache.py index 29a44a6..cfd6288 100644 --- a/mango/cache.py +++ b/mango/cache.py @@ -24,15 +24,15 @@ from .addressableaccount import AddressableAccount from .context import Context from .layouts import layouts from .metadata import Metadata +from .token import Token +from .tokenvalue import TokenValue from .version import Version -# # πŸ₯­ NodeBank class +# # πŸ₯­ PriceCache class # -# `NodeBank` stores details of deposits/borrows and vault. +# `PriceCache` stores a cached price. # - - class PriceCache: def __init__(self, price: Decimal, last_update: datetime): self.price: Decimal = price @@ -45,12 +45,16 @@ class PriceCache: return PriceCache(layout.price, layout.last_update) def __str__(self) -> str: - return f"Β« π™Ώπš›πš’πšŒπšŽπ™²πšŠπšŒπš‘πšŽ [{self.last_update}] {self.price:,.8f} Β»" + return f"Β« π™Ώπš›πš’πšŒπšŽπ™²πšŠπšŒπš‘πšŽ [{self.last_update}] {self.price:,.20f} Β»" def __repr__(self) -> str: return f"{self}" +# # πŸ₯­ RootBankCache class +# +# `RootBankCache` stores cached details of deposits and borrows. +# class RootBankCache: def __init__(self, deposit_index: Decimal, borrow_index: Decimal, last_update: datetime): self.deposit_index: Decimal = deposit_index @@ -64,12 +68,16 @@ class RootBankCache: return RootBankCache(layout.deposit_index, layout.borrow_index, layout.last_update) def __str__(self) -> str: - return f"Β« πšπš˜πš˜πšπ™±πšŠπš—πš”π™²πšŠπšŒπš‘πšŽ [{self.last_update}] {self.deposit_index:,.8f} / {self.borrow_index:,.8f} Β»" + return f"Β« πšπš˜πš˜πšπ™±πšŠπš—πš”π™²πšŠπšŒπš‘πšŽ [{self.last_update}] {self.deposit_index:,.20f} / {self.borrow_index:,.20f} Β»" def __repr__(self) -> str: return f"{self}" +# # πŸ₯­ PerpMarketCache class +# +# `PerpMarketCache` stores cached details of long and short funding. +# class PerpMarketCache: def __init__(self, long_funding: Decimal, short_funding: Decimal, last_update: datetime): self.long_funding: Decimal = long_funding @@ -83,7 +91,44 @@ class PerpMarketCache: return PerpMarketCache(layout.long_funding, layout.short_funding, layout.last_update) def __str__(self) -> str: - return f"Β« π™ΏπšŽπš›πš™π™ΌπšŠπš›πš”πšŽπšπ™²πšŠπšŒπš‘πšŽ [{self.last_update}] {self.long_funding:,.8f} / {self.short_funding:,.8f} Β»" + return f"Β« π™ΏπšŽπš›πš™π™ΌπšŠπš›πš”πšŽπšπ™²πšŠπšŒπš‘πšŽ [{self.last_update}] {self.long_funding:,.20f} / {self.short_funding:,.20f} Β»" + + def __repr__(self) -> str: + return f"{self}" + + +# # πŸ₯­ MarketCache class +# +# `MarketCache` stores cached details of price, root bank, and perp market, for a particular market. +# +class MarketCache: + def __init__(self, price: typing.Optional[PriceCache], root_bank: typing.Optional[RootBankCache], perp_market: typing.Optional[PerpMarketCache]): + self.price: typing.Optional[PriceCache] = price + self.root_bank: typing.Optional[RootBankCache] = root_bank + self.perp_market: typing.Optional[PerpMarketCache] = perp_market + + def adjusted_price(self, token: Token, quote_token: Token) -> TokenValue: + if token == quote_token: + # The price of 1 unit of the shared quote token is always 1. + return TokenValue(token, Decimal(1)) + + if self.price is None: + raise Exception(f"Could not find price index of basket token {token.symbol}.") + + price: Decimal = self.price.price + decimals_difference = token.decimals - quote_token.decimals + if decimals_difference != 0: + adjustment = 10 ** decimals_difference + price = price * adjustment + + return TokenValue(quote_token, price) + + def __str__(self) -> str: + return f"""Β« π™ΌπšŠπš›πš”πšŽπšπ™²πšŠπšŒπš‘πšŽ + {self.price} + {self.root_bank} + {self.perp_market} +Β»""" def __repr__(self) -> str: return f"{self}" @@ -93,7 +138,6 @@ class PerpMarketCache: # # `Cache` stores cache details of prices, root banks and perp markets. # - class Cache(AddressableAccount): def __init__(self, account_info: AccountInfo, version: Version, meta_data: Metadata, price_cache: typing.Sequence[typing.Optional[PriceCache]], @@ -136,6 +180,9 @@ class Cache(AddressableAccount): raise Exception(f"Cache account not found at address '{address}'") return Cache.parse(account_info) + def market_cache_for_index(self, index: int) -> MarketCache: + return MarketCache(self.price_cache[index], self.root_bank_cache[index], self.perp_market_cache[index]) + def __str__(self) -> str: def _render_list(items, stub): rendered = [] diff --git a/mango/calculators/healthcalculator.py b/mango/calculators/healthcalculator.py index 5b36250..dfa63d6 100644 --- a/mango/calculators/healthcalculator.py +++ b/mango/calculators/healthcalculator.py @@ -23,7 +23,7 @@ from mango.perpmarketinfo import PerpMarketInfo from ..account import Account, AccountBasketBaseToken from ..accounttokenvalues import AccountTokenValues, PricedAccountTokenValues -from ..cache import Cache, PerpMarketCache +from ..cache import Cache, MarketCache from ..context import Context from ..group import Group from ..lotsizeconverter import NullLotSizeConverter @@ -112,10 +112,12 @@ class HealthCalculator: # if (asset.deposit.value != 0) or (asset.borrow.value != 0) or (asset.net_value.value != 0): report: AccountTokenValues = AccountTokenValues.from_account_basket_base_token( asset, open_orders_by_address, group) - price: TokenValue = group.token_price_from_cache(cache, report.base_token) - perp_market_cache: typing.Optional[PerpMarketCache] = group.perp_market_cache_from_cache( - cache, report.base_token) - priced_report: PricedAccountTokenValues = report.priced(price, perp_market_cache) + # print("report", report) + # price: TokenValue = group.token_price_from_cache(cache, report.base_token) + market_cache: MarketCache = group.market_cache_from_cache(cache, report.base_token) + # print("Market cache", market_cache) + priced_report: PricedAccountTokenValues = report.priced(market_cache) + # print("priced_report", priced_report) priced_reports += [priced_report] quote_token_free_in_open_orders: TokenValue = TokenValue(group.shared_quote_token.token, Decimal(0)) @@ -123,20 +125,29 @@ class HealthCalculator: for priced_report in priced_reports: quote_token_free_in_open_orders += priced_report.quote_token_free quote_token_total_in_open_orders += priced_report.quote_token_total + # print("quote_token_free_in_open_orders", quote_token_free_in_open_orders) + # print("quote_token_total_in_open_orders", quote_token_total_in_open_orders) quote_report: AccountTokenValues = AccountTokenValues(account.shared_quote_token.token_info.token, account.shared_quote_token.token_info.token, + account.shared_quote_token.raw_deposit, account.shared_quote_token.deposit, + account.shared_quote_token.raw_borrow, account.shared_quote_token.borrow, TokenValue(group.shared_quote_token.token, Decimal(0)), TokenValue(group.shared_quote_token.token, Decimal(0)), quote_token_free_in_open_orders, quote_token_total_in_open_orders, TokenValue(group.shared_quote_token.token, Decimal(0)), - Decimal(0), Decimal(0), Decimal(0), + Decimal(0), Decimal(0), + TokenValue(group.shared_quote_token.token, Decimal(0)), + TokenValue(group.shared_quote_token.token, Decimal(0)), + Decimal(0), Decimal(0), NullLotSizeConverter()) + # print("quote_report", quote_report) health: Decimal = quote_report.net_value.value + # print("Health (start)", health) for priced_report in priced_reports: market_index = group.find_token_market_index(priced_report.base_token) spot_market: typing.Optional[SpotMarketInfo] = group.spot_markets[market_index] @@ -147,8 +158,9 @@ class HealthCalculator: spot_weight = spot_market.init_asset_weight if base_value > 0 else spot_market.init_liab_weight spot_health = base_value.value * spot_weight + # print("Weights", base_value.value, "*", spot_weight, spot_health) - perp_base, _ = self._calculate_pessimistic_perp_value(priced_report) + perp_base, perp_quote = priced_report.if_worst_execution() perp_market: typing.Optional[PerpMarketInfo] = group.perp_markets[market_index] perp_health: Decimal = Decimal(0) if perp_market is not None: @@ -158,7 +170,12 @@ class HealthCalculator: health += spot_health health += perp_health health += quote_value.value + health += perp_quote.value health += priced_report.raw_perp_quote_position + # print("Health (now)", health, spot_health, perp_health, quote_value.value, + # perp_quote.value, priced_report.raw_perp_quote_position) + + # print("Health (returning)", health) return health diff --git a/mango/group.py b/mango/group.py index 7ee6a72..f0ebcb4 100644 --- a/mango/group.py +++ b/mango/group.py @@ -20,7 +20,7 @@ from solana.publickey import PublicKey from .accountinfo import AccountInfo from .addressableaccount import AddressableAccount -from .cache import Cache, PriceCache, PerpMarketCache +from .cache import Cache, PerpMarketCache, MarketCache from .context import Context from .layouts import layouts from .lotsizeconverter import LotSizeConverter, RaisingLotSizeConverter @@ -275,26 +275,16 @@ class Group(AddressableAccount): raise Exception(f"Could not find token info for symbol '{symbol}' in group {self.address}") def token_price_from_cache(self, cache: Cache, token: Token) -> TokenValue: - if token == self.shared_quote_token.token: - # The price of 1 unit of the shared quote token is always 1. - return TokenValue(token, Decimal(1)) - - token_index: int = self.find_token_market_index(token) - cached_price: typing.Optional[PriceCache] = cache.price_cache[token_index] - if cached_price is None: - raise Exception(f"Could not find price index of basket token {token.symbol}.") - - price: Decimal = cached_price.price - decimals_difference = token.decimals - self.shared_quote_token.decimals - if decimals_difference != 0: - adjustment = 10 ** decimals_difference - price = price * adjustment - - return TokenValue(self.shared_quote_token.token, price) + market_cache: MarketCache = self.market_cache_from_cache(cache, token) + return market_cache.adjusted_price(token, self.shared_quote_token.token) def perp_market_cache_from_cache(self, cache: Cache, token: Token) -> typing.Optional[PerpMarketCache]: + market_cache: MarketCache = self.market_cache_from_cache(cache, token) + return market_cache.perp_market + + def market_cache_from_cache(self, cache: Cache, token: Token) -> MarketCache: token_index: int = self.find_token_market_index(token) - return cache.perp_market_cache[token_index] + return cache.market_cache_for_index(token_index) def fetch_balances(self, context: Context, root_address: PublicKey) -> typing.Sequence[TokenValue]: balances: typing.List[TokenValue] = [] diff --git a/mango/layouts/layouts.py b/mango/layouts/layouts.py index 3c79b32..1e71fb1 100644 --- a/mango/layouts/layouts.py +++ b/mango/layouts/layouts.py @@ -175,8 +175,8 @@ class FloatI80F48Adapter(construct.Adapter): def _decode(self, obj, context, path) -> Decimal: # How many decimal places precision should we allow for an I80F48? We could: - # return round(Decimal(obj) / self.divisor, 12) - return Decimal(obj) / self.divisor + return round(Decimal(obj) / self.divisor, 20) + # return Decimal(obj) / self.divisor def _encode(self, obj, context, path) -> bytes: return bytes(obj) diff --git a/mango/token.py b/mango/token.py index 7cec0a9..75352cb 100644 --- a/mango/token.py +++ b/mango/token.py @@ -41,7 +41,7 @@ class Token: def shift_to_decimals(self, value: Decimal) -> Decimal: divisor = Decimal(10 ** self.decimals) shifted = value / divisor - return round(shifted, int(self.decimals)) + return shifted def shift_to_native(self, value: Decimal) -> Decimal: multiplier = Decimal(10 ** self.decimals) diff --git a/tests/calculations/test_healthcalculator.py b/tests/calculations/test_healthcalculator.py index a70a0dd..799b4ed 100644 --- a/tests/calculations/test_healthcalculator.py +++ b/tests/calculations/test_healthcalculator.py @@ -1,5 +1,5 @@ from ..fakes import fake_context -from ..data import load_data_from_directory +from ..data import load_cache, load_data_from_directory from decimal import Decimal from mango.calculators.healthcalculator import HealthType, HealthCalculator @@ -15,15 +15,26 @@ def test_empty(): assert health == Decimal("0") +def test_1deposit(): + context = fake_context() + group, cache, account, open_orders = load_data_from_directory("tests/testdata/1deposit") + + actual = HealthCalculator(context, HealthType.INITIAL) + health = actual.calculate(account, open_orders, group, cache) + # Typescript says: 37904260000.05905822642118252475 + assert health == Decimal("37904.2600000591928892771752953600134") + + def test_perp_account_no_spot_openorders(): context = fake_context() group, cache, account, open_orders = load_data_from_directory("tests/testdata/perp_account_no_spot_openorders") actual = HealthCalculator(context, HealthType.INITIAL) health = actual.calculate(account, open_orders, group, cache) + # Typescript says: 341025333625.51856223547208912805 # TODO: This is significantly different from Typescript answer - assert health == Decimal("358923.807024760000053942349040880810") + assert health == Decimal("7036880.69722811087924538653007346763") def test_perp_account_no_spot_openorders_unhealthy(): @@ -35,7 +46,7 @@ def test_perp_account_no_spot_openorders_unhealthy(): health = actual.calculate(account, open_orders, group, cache) # Typescript says: -848086876487.04950427436299875694 # TODO: This is significantly different from Typescript answer - assert health == Decimal("-183567.651235339999859541052273925740") + assert health == Decimal("1100318.49506000114695611699892507857") def test_account1(): @@ -46,7 +57,7 @@ def test_account1(): health = actual.calculate(account, open_orders, group, cache) # Typescript says: 454884281.15520619643754685058 # TODO: This is slightly different from Typescript answer - assert health == Decimal("455.035346700961950901638175537300417") + assert health == Decimal("2578453.62441460502856112835667758626") def test_account2(): @@ -57,4 +68,4 @@ def test_account2(): health = actual.calculate(account, open_orders, group, cache) # Typescript says: 7516159604.84918334545095675026 # TODO: This is slightly different from Typescript answer - assert health == Decimal("7518.40764303100646658275181266617450") + assert health == Decimal("-34471.8824121736505777079119365978915") diff --git a/tests/test_account.py b/tests/test_account.py index 73b9e2f..714887d 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -13,21 +13,30 @@ def test_construction(): info = "some name" in_margin_basket = [False, False, False, False, False] active_in_basket = [False, True, False, True, True] - quote_deposit = fake_token_value(Decimal(50)) - quote_borrow = fake_token_value(Decimal(5)) - quote = mango.AccountBasketToken(fake_token_info(), quote_deposit, quote_borrow) - deposit1 = fake_token_value(Decimal(1)) - deposit2 = fake_token_value(Decimal(2)) - deposit3 = fake_token_value(Decimal(3)) - borrow1 = fake_token_value(Decimal("0.1")) - borrow2 = fake_token_value(Decimal("0.2")) - borrow3 = fake_token_value(Decimal("0.3")) + raw_quote_deposit = Decimal(50) + quote_deposit = fake_token_value(raw_quote_deposit) + raw_quote_borrow = Decimal(5) + quote_borrow = fake_token_value(raw_quote_borrow) + quote = mango.AccountBasketToken(fake_token_info(), raw_quote_deposit, + quote_deposit, raw_quote_borrow, quote_borrow) + raw_deposit1 = Decimal(1) + deposit1 = fake_token_value(raw_deposit1) + raw_deposit2 = Decimal(2) + deposit2 = fake_token_value(raw_deposit2) + raw_deposit3 = Decimal(3) + deposit3 = fake_token_value(raw_deposit3) + raw_borrow1 = Decimal("0.1") + borrow1 = fake_token_value(raw_borrow1) + raw_borrow2 = Decimal("0.2") + borrow2 = fake_token_value(raw_borrow2) + raw_borrow3 = Decimal("0.3") + borrow3 = fake_token_value(raw_borrow3) basket = [ - mango.AccountBasketBaseToken(fake_token_info(), fake_token_info(), deposit1, borrow1, + mango.AccountBasketBaseToken(fake_token_info(), fake_token_info(), raw_deposit1, deposit1, raw_borrow1, borrow1, fake_seeded_public_key("spot openorders 1"), fake_seeded_public_key("perp1")), - mango.AccountBasketBaseToken(fake_token_info(), fake_token_info(), deposit2, borrow2, + mango.AccountBasketBaseToken(fake_token_info(), fake_token_info(), raw_deposit2, deposit2, raw_borrow2, borrow2, fake_seeded_public_key("spot openorders 2"), fake_seeded_public_key("perp2")), - mango.AccountBasketBaseToken(fake_token_info(), fake_token_info(), deposit3, borrow3, + mango.AccountBasketBaseToken(fake_token_info(), fake_token_info(), raw_deposit3, deposit3, raw_borrow3, borrow3, fake_seeded_public_key("spot openorders 3"), fake_seeded_public_key("perp3")), ] msrm_amount = Decimal(0) diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 0000000..bb219ad --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,102 @@ +from .context import mango +from .fakes import fake_account_info, fake_seeded_public_key +from .data import load_cache + +from datetime import datetime +from decimal import Decimal + + +def test_cache_constructor(): + account_info = fake_account_info(fake_seeded_public_key("cache")) + meta_data = mango.Metadata(mango.layouts.DATA_TYPE.parse(bytearray(b'\x07')), mango.Version.V1, True) + timestamp = datetime.now() + price_cache = [mango.PriceCache(Decimal(26), timestamp)] + root_bank_cache = [mango.RootBankCache(Decimal("0.00001"), Decimal("0.00001"), timestamp)] + perp_market_cache = [mango.PerpMarketCache(Decimal("0.00002"), Decimal("0.00002"), timestamp)] + actual = mango.Cache(account_info, mango.Version.V1, meta_data, price_cache, root_bank_cache, perp_market_cache) + + assert actual is not None + assert actual.logger is not None + assert actual.account_info == account_info + assert actual.address == fake_seeded_public_key("cache") + assert actual.meta_data == meta_data + assert actual.meta_data.data_type == mango.layouts.DATA_TYPE.Cache + assert actual.price_cache == price_cache + assert actual.root_bank_cache == root_bank_cache + assert actual.perp_market_cache == perp_market_cache + + +def test_load_cache(): + cache = load_cache("tests/testdata/1deposit/cache.json") + + # + # These values are all verified with the same file loaded in the TypeScript client. + # + + assert cache.price_cache[0].price == Decimal("0.33642499999999841975") + assert cache.price_cache[1].price == Decimal("47380.32499999999999928946") + assert cache.price_cache[2].price == Decimal("3309.69549999999999911893") + assert cache.price_cache[3].price == Decimal("0.17261599999999788224") + assert cache.price_cache[4].price == Decimal("8.79379999999999739657") + assert cache.price_cache[5].price == Decimal("1") + assert cache.price_cache[6].price == Decimal("1.00039999999999906777") + assert cache.price_cache[7] is None + assert cache.price_cache[8] is None + assert cache.price_cache[9] is None + assert cache.price_cache[10] is None + assert cache.price_cache[11] is None + assert cache.price_cache[12] is None + assert cache.price_cache[13] is None + assert cache.price_cache[14] is None + + assert cache.root_bank_cache[0].deposit_index == Decimal("1001923.86460821722014813417") + assert cache.root_bank_cache[0].borrow_index == Decimal("1002515.45257855337824182129") + assert cache.root_bank_cache[1].deposit_index == Decimal("1000007.37249653914441083202") + assert cache.root_bank_cache[1].borrow_index == Decimal("1000166.98522159213999316307") + assert cache.root_bank_cache[2].deposit_index == Decimal("1000000.19554886875829424753") + assert cache.root_bank_cache[2].borrow_index == Decimal("1000001.13273253565107623331") + assert cache.root_bank_cache[3].deposit_index == Decimal("1000037.82149923799070379005") + assert cache.root_bank_cache[3].borrow_index == Decimal("1000044.28925241010965052624") + assert cache.root_bank_cache[4].deposit_index == Decimal("1000000.0000132182767842437") + assert cache.root_bank_cache[4].borrow_index == Decimal("1000000.14235973938041368569") + assert cache.root_bank_cache[5].deposit_index == Decimal("1000000.35244386506945346582") + assert cache.root_bank_cache[5].borrow_index == Decimal("1000000.66156146420993522383") + assert cache.root_bank_cache[6].deposit_index == Decimal("1000473.25161608998580575758") + assert cache.root_bank_cache[6].borrow_index == Decimal("1000524.37279217702128875089") + assert cache.root_bank_cache[7] is None + assert cache.root_bank_cache[7] is None + assert cache.root_bank_cache[8] is None + assert cache.root_bank_cache[8] is None + assert cache.root_bank_cache[9] is None + assert cache.root_bank_cache[9] is None + assert cache.root_bank_cache[10] is None + assert cache.root_bank_cache[10] is None + assert cache.root_bank_cache[11] is None + assert cache.root_bank_cache[11] is None + assert cache.root_bank_cache[12] is None + assert cache.root_bank_cache[12] is None + assert cache.root_bank_cache[13] is None + assert cache.root_bank_cache[13] is None + assert cache.root_bank_cache[14] is None + assert cache.root_bank_cache[14] is None + assert cache.root_bank_cache[15].deposit_index == Decimal("1000154.42276607534055088422") + assert cache.root_bank_cache[15].borrow_index == Decimal("1000219.00868743509063563124") + + assert cache.perp_market_cache[0] is None + assert cache.perp_market_cache[1].long_funding == Decimal("-751864.70031280454435673732") + assert cache.perp_market_cache[1].short_funding == Decimal("-752275.3557979761382519257") + assert cache.perp_market_cache[2].long_funding == Decimal("0") + assert cache.perp_market_cache[2].short_funding == Decimal("0") + assert cache.perp_market_cache[3].long_funding == Decimal("-636425.51790158202868497028") + assert cache.perp_market_cache[3].short_funding == Decimal("-636425.51790158202868497028") + assert cache.perp_market_cache[4] is None + assert cache.perp_market_cache[5] is None + assert cache.perp_market_cache[6] is None + assert cache.perp_market_cache[7] is None + assert cache.perp_market_cache[8] is None + assert cache.perp_market_cache[9] is None + assert cache.perp_market_cache[10] is None + assert cache.perp_market_cache[11] is None + assert cache.perp_market_cache[12] is None + assert cache.perp_market_cache[13] is None + assert cache.perp_market_cache[14] is None diff --git a/tests/test_rootbank.py b/tests/test_rootbank.py index 8cdb075..dd9fdfc 100644 --- a/tests/test_rootbank.py +++ b/tests/test_rootbank.py @@ -54,8 +54,8 @@ def test_root_bank_constructor(): assert actual.last_updated == timestamp -def test_root_bank_loaded(): - actual = load_root_bank("tests/testdata/empty/root_bank0.json") +def test_load_root_bank(): + actual = load_root_bank("tests/testdata/1deposit/root_bank0.json") assert actual is not None assert actual.logger is not None @@ -63,16 +63,10 @@ def test_root_bank_loaded(): assert actual.meta_data.version == mango.Version.V1 assert actual.meta_data.data_type == mango.layouts.DATA_TYPE.RootBank assert actual.meta_data.is_initialized - # Typescript says: 0.69999999999999928946 - assert actual.optimal_util == Decimal("0.699999999999999289457264239899814129") - # Typescript says: 0.05999999999999872102 - assert actual.optimal_rate == Decimal("0.0599999999999987210230756318196654320") - # Typescript says: 1.5 + assert actual.optimal_util == Decimal("0.69999999999999928946") + assert actual.optimal_rate == Decimal("0.05999999999999872102") assert actual.max_rate == Decimal("1.5") assert actual.node_banks[0] == PublicKey("J2Lmnc1e4frMnBEJARPoHtfpcohLfN67HdK1inXjTFSM") - # Typescript says: 1000154.42276607355830719825 - assert actual.deposit_index == Decimal("1000154.42276607355830719825462438166") - # Typescript says: 1000219.00867863010088498754 - assert actual.borrow_index == Decimal("1000219.00867863010088498754157626536") - # Typescript says: "Mon, 04 Oct 2021 14:58:05 GMT" + assert actual.deposit_index == Decimal("1000154.42276607355830719825") + assert actual.borrow_index == Decimal("1000219.00867863010088498754") assert actual.last_updated == datetime(2021, 10, 4, 14, 58, 5, 0, timezone.utc)