diff --git a/bin/hedger b/bin/hedger deleted file mode 100755 index a555fc0..0000000 --- a/bin/hedger +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env pyston3 - -import argparse -import logging -import os -import os.path -import rx.operators -import sys -import threading - -from decimal import Decimal -from rx.core.abc.disposable import Disposable -from solana.publickey import PublicKey - -sys.path.insert(0, os.path.abspath( - os.path.join(os.path.dirname(__file__), ".."))) -import mango # nopep8 -import mango.layouts # nopep8 -import mango.marketmaking # nopep8 - -parser = argparse.ArgumentParser( - description="Hedges perp purchases by trading the underlying in the opposite direction.") -mango.ContextBuilder.add_command_line_parameters(parser) -mango.Wallet.add_command_line_parameters(parser) -parser.add_argument("--watch-market", type=str, required=True, help="perp market symbol to hedge (e.g. ETH-PERP)") -parser.add_argument("--hedge-market", type=str, required=True, help="spot market symbol to hedge (e.g. ETH/USDC)") -parser.add_argument("--max-price-slippage-factor", type=Decimal, default=Decimal("0.05"), - help="the maximum value the IOC hedging order price can slip by when hedging (default is 0.05 for 5%%)") -parser.add_argument("--notify-errors", type=mango.parse_subscription_target, action="append", default=[], - help="The notification target for error events") -parser.add_argument("--account-address", type=PublicKey, - help="address of the specific account to use, if more than one available") -parser.add_argument("--dry-run", action="store_true", default=False, - help="runs as read-only and does not perform any transactions") -args: argparse.Namespace = mango.parse_args(parser) - -logging.getLogger().setLevel(args.log_level) -for notify in args.notify_errors: - handler = mango.NotificationHandler(notify) - handler.setLevel(logging.ERROR) - logging.getLogger().addHandler(handler) - - -context = mango.ContextBuilder.from_command_line_parameters(args) -wallet = mango.Wallet.from_command_line_parameters_or_raise(args) -group = mango.Group.load(context, context.group_address) -account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address) - -disposer = mango.DisposePropagator() -manager = mango.IndividualWebSocketSubscriptionManager(context) -disposer.add_disposable(manager) -health_check = mango.HealthCheck() -disposer.add_disposable(health_check) - -watched_market_symbol = args.watch_market.upper() -watched_market_stub = context.market_lookup.find_by_symbol(watched_market_symbol) -if watched_market_stub is None: - raise Exception(f"Could not find market {watched_market_symbol}") - -ensured_watched_market = mango.ensure_market_loaded(context, watched_market_stub) -if not isinstance(ensured_watched_market, mango.PerpMarket): - raise Exception(f"Market {watched_market_symbol} is not a perp market.") -watched_market: mango.PerpMarket = ensured_watched_market - -hedging_market_symbol = args.hedge_market.upper() -hedging_market_stub = context.market_lookup.find_by_symbol(hedging_market_symbol) -if hedging_market_stub is None: - raise Exception(f"Could not find market {hedging_market_symbol}") - -hedging_market = mango.ensure_market_loaded(context, hedging_market_stub) -if not isinstance(hedging_market, mango.SpotMarket): - raise Exception(f"Market {hedging_market_symbol} is not a spot market.") - -hedging_market_operations: mango.MarketOperations = mango.create_market_operations( - context, wallet, account, hedging_market, args.dry_run) - -perp_event_queue: mango.PerpEventQueue = mango.PerpEventQueue.load( - context, watched_market.underlying_perp_market.event_queue, watched_market.lot_size_converter) - -event_subscription = mango.WebSocketAccountSubscription( - context, watched_market.underlying_perp_market.event_queue, lambda account_info: mango.PerpEventQueue.parse(account_info, watched_market.lot_size_converter)) -manager.add(event_subscription) -health_check.add("hedger_watched_events_pong_subscription", event_subscription.pong) - -splitter: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker(perp_event_queue) -hedger: mango.PerpHedger = mango.PerpHedger( - account.address, hedging_market_operations, args.max_price_slippage_factor) - -hedger_subscription: Disposable = event_subscription.publisher.pipe( - # Passes on distinct unseen events - rx.operators.flat_map(splitter.unseen), - - # Only fills after this filter - rx.operators.filter(lambda unseen_event: isinstance(unseen_event, mango.PerpFillEvent)), - - # Only fills from our account after this filter - rx.operators.filter(lambda fill: (fill.maker == account.address) or (fill.taker == account.address)), - - # Only fills where we didn't trade with outself - rx.operators.filter(lambda fill: fill.maker != fill.taker), - - # We've got a fill we want to hedge, so do it. - rx.operators.map(lambda fill: hedger.hedge(context, fill)) -).subscribe(mango.PrintingObserverSubscriber(False)) - -disposer.add_disposable(hedger_subscription) -manager.open() - -logging.info(f"Current assets in account {account.address} (owner: {account.owner}):") -mango.TokenValue.report([asset for asset in account.net_assets if asset is not None], logging.info) - -# Wait - don't exit. Exiting will be handled by signals/interrupts. -waiter = threading.Event() -try: - waiter.wait() -except: - pass - -logging.info("Shutting down...") -disposer.dispose() -logging.info("Shutdown complete.") diff --git a/bin/marketmaker b/bin/marketmaker index 649aeb0..abc73b2 100755 --- a/bin/marketmaker +++ b/bin/marketmaker @@ -10,12 +10,12 @@ import sys import threading from decimal import Decimal -from rx.core.abc.disposable import Disposable from solana.publickey import PublicKey sys.path.insert(0, os.path.abspath( os.path.join(os.path.dirname(__file__), ".."))) import mango # nopep8 +import mango.hedging # nopep8 import mango.marketmaking # nopep8 from mango.marketmaking.orderchain import chain # nopep8 from mango.marketmaking.orderchain import chainbuilder # nopep8 @@ -97,10 +97,12 @@ if market.quote != group.shared_quote_token.token: cleanup(context, wallet, account, market, args.dry_run) +hedger: mango.hedging.Hedger = mango.hedging.NullHedger() if args.hedge_market is not None: if not isinstance(market, mango.PerpMarket): raise Exception(f"Cannot hedge - market {market.symbol} is not a perp market.") - watched_market: mango.PerpMarket = market + + underlying_market: mango.PerpMarket = market hedging_market_symbol = args.hedge_market.upper() hedging_market_stub = context.market_lookup.find_by_symbol(hedging_market_symbol) @@ -116,51 +118,8 @@ if args.hedge_market is not None: hedging_market_operations: mango.MarketOperations = mango.create_market_operations( context, wallet, account, hedging_market, args.dry_run) - perp_event_queue: mango.PerpEventQueue = mango.PerpEventQueue.load( - context, watched_market.underlying_perp_market.event_queue, watched_market.lot_size_converter) - - event_queue_source: mango.EventSource[mango.PerpEventQueue] - if args.update_mode == mango.marketmaking.ModelUpdateMode.WEBSOCKET: - event_subscription = mango.WebSocketAccountSubscription( - context, watched_market.underlying_perp_market.event_queue, - lambda account_info: mango.PerpEventQueue.parse(account_info, watched_market.lot_size_converter)) - manager.add(event_subscription) - health_check.add("hedger_watched_events_pong_subscription", event_subscription.pong) - event_queue_source = event_subscription.publisher - else: - event_queue_observable: rx.Observable = rx.interval(args.event_queue_poll_interval).pipe( - rx.operators.observe_on(context.create_thread_pool_scheduler()), - rx.operators.catch(mango.observable_pipeline_error_reporter), - rx.operators.retry(), - rx.operators.start_with(-1), - rx.operators.map(lambda _: mango.PerpEventQueue.load( - context, watched_market.underlying_perp_market.event_queue, watched_market.lot_size_converter)) - ) - event_queue_source = mango.EventSource[mango.PerpEventQueue]() - event_queue_observable.subscribe(event_queue_source) - health_check.add("hedger_watched_events_pong_subscription", event_queue_source) - - splitter: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker(perp_event_queue) - hedger: mango.PerpHedger = mango.PerpHedger( - account.address, hedging_market_operations, args.max_price_slippage_factor) - - hedger_subscription: Disposable = event_queue_observable.pipe( - # Passes on distinct unseen events - rx.operators.flat_map(splitter.unseen), - - # Only fills after this filter - rx.operators.filter(lambda unseen_event: isinstance(unseen_event, mango.PerpFillEvent)), - - # Only fills from our account after this filter - rx.operators.filter(lambda fill: (fill.maker == account.address) or (fill.taker == account.address)), - - # Only fills where we didn't trade with outself - rx.operators.filter(lambda fill: fill.maker != fill.taker), - - # We've got a fill we want to hedge, so do it. - rx.operators.map(lambda fill: hedger.hedge(context, fill)) - ).subscribe(mango.PrintingObserverSubscriber(False)) - disposer.add_disposable(hedger_subscription) + hedger = mango.hedging.PerpToSpotHedger(group, underlying_market, hedging_market, + hedging_market_operations, args.max_price_slippage_factor) order_reconciler = mango.marketmaking.ToleranceOrderReconciler( @@ -191,13 +150,20 @@ mango.TokenValue.report([asset for asset in account.net_assets if asset is not N manager.open() + +def pulse_action(_) -> None: + model_state: mango.ModelState = model_state_builder.build(context) + market_maker.pulse(context, model_state) + hedger.pulse(context, model_state) + + pulse_disposable = rx.interval(args.pulse_interval).pipe( rx.operators.observe_on(context.create_thread_pool_scheduler()), rx.operators.start_with(-1), rx.operators.catch(mango.observable_pipeline_error_reporter), rx.operators.retry() ).subscribe( - on_next=lambda _: market_maker.pulse(context, model_state_builder.build(context))) + on_next=pulse_action) disposer.add_disposable(pulse_disposable) # Wait - don't exit. Exiting will be handled by signals/interrupts. diff --git a/bin/redeem-mango b/bin/redeem-mango index 144e38a..8e44dff 100755 --- a/bin/redeem-mango +++ b/bin/redeem-mango @@ -6,6 +6,7 @@ import os.path import sys import typing +from decimal import Decimal from solana.publickey import PublicKey sys.path.insert(0, os.path.abspath( @@ -15,11 +16,16 @@ import mango # nopep8 def report_accrued(basket_token: mango.AccountBasketBaseToken): symbol: str = basket_token.token_info.token.symbol - accrued: mango.TokenValue = basket_token.perp_account.mngo_accrued + if basket_token.perp_account is None: + accrued: mango.TokenValue = mango.TokenValue(basket_token.token_info.token, Decimal(0)) + else: + accrued = basket_token.perp_account.mngo_accrued print(f"Accrued in perp market [{symbol:>5}]: {accrued}") def load_perp_market(context: mango.Context, group: mango.Group, group_basket_market: mango.GroupBasketMarket): + if group_basket_market.perp_market_info is None: + raise Exception(f"No perp market available for group basket market: {group_basket_market}") perp_market_details = mango.PerpMarketDetails.load(context, group_basket_market.perp_market_info.address, group) perp_market = mango.PerpMarket(context.mango_program_address, group_basket_market.perp_market_info.address, group_basket_market.base_token_info.token, diff --git a/mango/__init__.py b/mango/__init__.py index d57bf17..e1f629c 100644 --- a/mango/__init__.py +++ b/mango/__init__.py @@ -37,6 +37,7 @@ from .marketinstructionbuilder import MarketInstructionBuilder, NullMarketInstru from .marketlookup import MarketLookup, NullMarketLookup, CompoundMarketLookup from .marketoperations import MarketOperations, DryRunMarketOperations from .metadata import Metadata +from .modelstate import ModelState from .notification import NotificationTarget, TelegramNotificationTarget, DiscordNotificationTarget, MailjetNotificationTarget, CsvFileNotificationTarget, FilteringNotificationTarget, NotificationHandler, parse_subscription_target from .observables import DisposePropagator, DisposeWrapper, NullObserverSubscriber, PrintingObserverSubscriber, TimestampedPrintingObserverSubscriber, CollectingObserverSubscriber, LatestItemObserverSubscriber, CaptureFirstItem, FunctionObserver, create_backpressure_skipping_observer, debug_print_item, log_subscription_error, observable_pipeline_error_reporter, EventSource from .openorders import OpenOrders diff --git a/mango/account.py b/mango/account.py index dcc2964..decb65e 100644 --- a/mango/account.py +++ b/mango/account.py @@ -137,6 +137,10 @@ class Account(AddressableAccount): placed_order = PlacedOrder(id, client_id, side) placed_orders_all_markets[int(order_market)] += [placed_order] + quote_token_info: typing.Optional[TokenInfo] = group.tokens[-1] + if quote_token_info is None: + raise Exception(f"Could not determine quote token in group {group.address}") + for index, token_info in enumerate(group.tokens[:-1]): if token_info: intrinsic_deposit = token_info.root_bank.deposit_index * layout.deposits[index] @@ -144,8 +148,16 @@ class Account(AddressableAccount): intrinsic_borrow = token_info.root_bank.borrow_index * layout.borrows[index] 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] + if group_basket_market is None: + raise Exception(f"Could not find group basket market at index {index}.") perp_account = PerpAccount.from_layout( - layout.perp_accounts[index], perp_open_orders, mngo_token_info.token) + layout.perp_accounts[index], + token_info.token, + quote_token_info.token, + perp_open_orders, + group_basket_market.perp_lot_size_converter, + mngo_token_info.token) spot_open_orders = layout.spot_open_orders[index] basket_item: AccountBasketBaseToken = AccountBasketBaseToken( token_info, deposit, borrow, spot_open_orders, perp_account) @@ -154,10 +166,6 @@ class Account(AddressableAccount): else: active_in_basket += [False] - quote_token_info: typing.Optional[TokenInfo] = group.tokens[-1] - if quote_token_info is None: - raise Exception(f"Could not determine quote token in group {group.address}") - intrinsic_quote_deposit = quote_token_info.root_bank.deposit_index * layout.deposits[-1] quote_deposit = TokenValue(quote_token_info.token, quote_token_info.token.shift_to_decimals(intrinsic_quote_deposit)) diff --git a/mango/group.py b/mango/group.py index 139e3d5..0f9f93f 100644 --- a/mango/group.py +++ b/mango/group.py @@ -22,6 +22,7 @@ from .accountinfo import AccountInfo from .addressableaccount import AddressableAccount from .context import Context from .layouts import layouts +from .lotsizeconverter import LotSizeConverter, RaisingLotSizeConverter from .marketlookup import MarketLookup from .metadata import Metadata from .perpmarketinfo import PerpMarketInfo @@ -39,11 +40,12 @@ from .version import Version # `GroupBasketMarket` gathers basket items together instead of separate arrays. # class GroupBasketMarket: - def __init__(self, base_token_info: TokenInfo, quote_token_info: TokenInfo, spot_market_info: SpotMarketInfo, perp_market_info: PerpMarketInfo, oracle: PublicKey): + def __init__(self, base_token_info: TokenInfo, quote_token_info: TokenInfo, spot_market_info: SpotMarketInfo, perp_market_info: typing.Optional[PerpMarketInfo], perp_lot_size_converter: LotSizeConverter, oracle: PublicKey): self.base_token_info: TokenInfo = base_token_info self.quote_token_info: TokenInfo = quote_token_info self.spot_market_info: SpotMarketInfo = spot_market_info - self.perp_market_info: PerpMarketInfo = perp_market_info + self.perp_market_info: typing.Optional[PerpMarketInfo] = perp_market_info + self.perp_lot_size_converter: LotSizeConverter = perp_lot_size_converter self.oracle: PublicKey = oracle def __str__(self) -> str: @@ -141,11 +143,24 @@ class Group(AddressableAccount): in_basket: typing.List[bool] = [] for index, base_token_info in enumerate(tokens[:-1]): if base_token_info is not None: - spot_market_info: SpotMarketInfo = SpotMarketInfo.from_layout(layout.spot_markets[index]) - perp_market_info: PerpMarketInfo = PerpMarketInfo.from_layout(layout.perp_markets[index]) + spot_market_info: typing.Optional[SpotMarketInfo] = SpotMarketInfo.from_layout_or_none( + layout.spot_markets[index]) + if spot_market_info is None: + raise Exception(f"Could not find spot market at index {index} of group layout.") + # spot_lot_size_converter: LotSizeConverter = RaisingLotSizeConverter() + # if spot_market_info is not None: + # spot_lot_size_converter = LotSizeConverter( + # base_token_info.token, spot_market_info.base_lot_size, quote_token_info.token, spot_market_info.) + perp_market_info: typing.Optional[PerpMarketInfo] = PerpMarketInfo.from_layout_or_none( + layout.perp_markets[index]) + perp_lot_size_converter: LotSizeConverter = RaisingLotSizeConverter() + if perp_market_info is not None: + perp_lot_size_converter = LotSizeConverter( + base_token_info.token, perp_market_info.base_lot_size, quote_token_info.token, perp_market_info.quote_lot_size) + oracle: PublicKey = layout.oracles[index] item: GroupBasketMarket = GroupBasketMarket( - base_token_info, quote_token_info, spot_market_info, perp_market_info, oracle) + base_token_info, quote_token_info, spot_market_info, perp_market_info, perp_lot_size_converter, oracle) basket += [item] in_basket += [True] else: diff --git a/mango/hedging/__init__.py b/mango/hedging/__init__.py new file mode 100644 index 0000000..d9c1d4c --- /dev/null +++ b/mango/hedging/__init__.py @@ -0,0 +1,3 @@ +from .hedger import Hedger +from .nullhedger import NullHedger +from .perptospothedger import PerpToSpotHedger diff --git a/mango/hedging/hedger.py b/mango/hedging/hedger.py new file mode 100644 index 0000000..04c3e9d --- /dev/null +++ b/mango/hedging/hedger.py @@ -0,0 +1,42 @@ +# # ⚠ 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 mango + +from datetime import datetime + +from ..observables import EventSource + + +# # πŸ₯­ Hedger class +# +# A base hedger class to allow hedging across markets. +# +class Hedger(metaclass=abc.ABCMeta): + def __init__(self): + self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) + self.pulse_complete: EventSource[datetime] = EventSource[datetime]() + self.pulse_error: EventSource[Exception] = EventSource[Exception]() + + def pulse(self, context: mango.Context, model_state: mango.ModelState): + raise NotImplementedError("Hedger.pulse() is not implemented on the base type.") + + def __str__(self) -> str: + return "Β« π™·πšŽπšπšπšŽπš› Β»" + + def __repr__(self) -> str: + return f"{self}" diff --git a/mango/hedging/nullhedger.py b/mango/hedging/nullhedger.py new file mode 100644 index 0000000..f81773a --- /dev/null +++ b/mango/hedging/nullhedger.py @@ -0,0 +1,36 @@ +# # ⚠ 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 mango + +from .hedger import Hedger + + +# # πŸ₯­ Hedger class +# +# A base hedger class to allow hedging across markets. +# +class NullHedger(Hedger): + def __init__(self): + super().__init__() + + def pulse(self, context: mango.Context, model_state: mango.ModelState): + pass + + def __str__(self) -> str: + return "Β« π™½πšžπš•πš•π™·πšŽπšπšπšŽπš› Β»" + + def __repr__(self) -> str: + return f"{self}" diff --git a/mango/hedging/perptospothedger.py b/mango/hedging/perptospothedger.py new file mode 100644 index 0000000..24d5d82 --- /dev/null +++ b/mango/hedging/perptospothedger.py @@ -0,0 +1,95 @@ +# # ⚠ 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 mango +import traceback +import typing + +from datetime import datetime +from decimal import Decimal + +from .hedger import Hedger + + +# # πŸ₯­ PerpToSpotHedger class +# +# A hedger that hedges perp positions using a spot market. +# +class PerpToSpotHedger(Hedger): + def __init__(self, group: mango.Group, underlying_market: mango.PerpMarket, + hedging_market: mango.SpotMarket, market_operations: mango.MarketOperations, + max_price_slippage_factor: Decimal): + super().__init__() + self.underlying_market: mango.PerpMarket = underlying_market + self.hedging_market: mango.SpotMarket = hedging_market + self.market_operations: mango.MarketOperations = market_operations + self.buy_price_adjustment_factor: Decimal = Decimal("1") + max_price_slippage_factor + self.sell_price_adjustment_factor: Decimal = Decimal("1") - max_price_slippage_factor + self.market_index: int = group.find_perp_market_index(underlying_market.address) + + def pulse(self, context: mango.Context, model_state: mango.ModelState): + try: + perp_account: typing.Optional[mango.PerpAccount] = model_state.account.perp_accounts[self.market_index] + if perp_account is None: + raise Exception( + f"Could not find perp account at index {self.market_index} in account {model_state.account.address}.") + + basket_token: typing.Optional[mango.AccountBasketToken] = model_state.account.basket_tokens[self.market_index] + if basket_token is None: + raise Exception( + f"Could not find basket token at index {self.market_index} in account {model_state.account.address}.") + + token_balance: mango.TokenValue = basket_token.net_value + perp_position: mango.TokenValue = perp_account.base_token_value + + # We're interested in maintaining the right size of hedge lots, so round everything to the hedge + # market's lot size (even though perps have different lot sizes). + perp_position_rounded: Decimal = self.hedging_market.lot_size_converter.round_base(perp_position.value) + token_balance_rounded: Decimal = self.hedging_market.lot_size_converter.round_base(token_balance.value) + + # When we add the rounded perp position and token balances, we should get zero if we're delta-neutral. + delta: Decimal = perp_position_rounded + token_balance_rounded + self.logger.debug( + f"Delta from {self.underlying_market.symbol} to {self.hedging_market.symbol} is {delta:,.8f} {basket_token.token_info.token.symbol}") + + if delta != 0: + side: mango.Side = mango.Side.BUY if delta < 0 else mango.Side.SELL + up_or_down: str = "up to" if side == mango.Side.BUY else "down to" + price_adjustment_factor: Decimal = self.sell_price_adjustment_factor if side == mango.Side.SELL else self.buy_price_adjustment_factor + + adjusted_price: Decimal = model_state.price.mid_price * price_adjustment_factor + quantity: Decimal = abs(delta) + order: mango.Order = mango.Order.from_basic_info(side, adjusted_price, quantity, mango.OrderType.IOC) + self.logger.info( + f"Hedging perp position {perp_position} and token balance {token_balance} with {side} of {quantity:,.8f} at {up_or_down} {adjusted_price:,.8f} on {self.hedging_market.symbol}\n\t{order}") + try: + self.market_operations.place_order(order) + except Exception: + self.logger.error( + f"[{context.name}] Failed to hedge on {self.hedging_market.symbol} using order {order} - {traceback.format_exc()}") + raise + + self.pulse_complete.on_next(datetime.now()) + except (mango.RateLimitException, mango.NodeIsBehindException, mango.BlockhashNotFoundException, mango.FailedToFetchBlockhashException) as common_exception: + # Don't bother with a long traceback for these common problems. + self.logger.error(f"[{context.name}] Hedger problem on pulse: {common_exception}") + self.pulse_error.on_next(common_exception) + except Exception as exception: + self.logger.error(f"[{context.name}] Hedger error on pulse:\n{traceback.format_exc()}") + self.pulse_error.on_next(exception) + + def __str__(self) -> str: + return f"Β« π™ΏπšŽπš›πš™πšƒπš˜πš‚πš™πš˜πšπ™·πšŽπšπšπšŽπš› for underlying '{self.underlying_market.symbol}', hedging on '{self.hedging_market.symbol}' Β»" diff --git a/mango/marketmaking/__init__.py b/mango/marketmaking/__init__.py index cb79e7a..98c5a46 100644 --- a/mango/marketmaking/__init__.py +++ b/mango/marketmaking/__init__.py @@ -1,5 +1,4 @@ from .marketmaker import MarketMaker -from .modelstate import ModelState from .modelstatebuilder import ModelStateBuilder, WebsocketModelStateBuilder, PollingModelStateBuilder, SerumPollingModelStateBuilder, SpotPollingModelStateBuilder, PerpPollingModelStateBuilder from .modelstatebuilderfactory import ModelUpdateMode, model_state_builder_factory from .orderreconciler import OrderReconciler, NullOrderReconciler diff --git a/mango/marketmaking/marketmaker.py b/mango/marketmaking/marketmaker.py index 86d308e..0e46454 100644 --- a/mango/marketmaking/marketmaker.py +++ b/mango/marketmaking/marketmaker.py @@ -22,7 +22,6 @@ import typing from datetime import datetime from decimal import Decimal -from .modelstate import ModelState from ..observables import EventSource from .orderreconciler import OrderReconciler from .orderchain.chain import Chain @@ -51,7 +50,7 @@ class MarketMaker: self.buy_client_ids: typing.List[int] = [] self.sell_client_ids: typing.List[int] = [] - def pulse(self, context: mango.Context, model_state: ModelState): + def pulse(self, context: mango.Context, model_state: mango.ModelState): try: payer = mango.CombinableInstructions.from_wallet(self.wallet) diff --git a/mango/marketmaking/modelstatebuilder.py b/mango/marketmaking/modelstatebuilder.py index 447747b..1fcf73f 100644 --- a/mango/marketmaking/modelstatebuilder.py +++ b/mango/marketmaking/modelstatebuilder.py @@ -22,7 +22,7 @@ import typing from decimal import Decimal from solana.publickey import PublicKey -from .modelstate import ModelState +from ..modelstate import ModelState from ..tokenvalue import TokenValue diff --git a/mango/marketmaking/modelstatebuilderfactory.py b/mango/marketmaking/modelstatebuilderfactory.py index 0f57ff6..c43b31e 100644 --- a/mango/marketmaking/modelstatebuilderfactory.py +++ b/mango/marketmaking/modelstatebuilderfactory.py @@ -20,7 +20,7 @@ import typing from solana.publickey import PublicKey from ..constants import SYSTEM_PROGRAM_ADDRESS -from .modelstate import ModelState +from ..modelstate import ModelState from .modelstatebuilder import ModelStateBuilder, WebsocketModelStateBuilder, SerumPollingModelStateBuilder, SpotPollingModelStateBuilder, PerpPollingModelStateBuilder diff --git a/mango/marketmaking/orderchain/afteraccumulateddepthelement.py b/mango/marketmaking/orderchain/afteraccumulateddepthelement.py index 2b54b15..db0a3d2 100644 --- a/mango/marketmaking/orderchain/afteraccumulateddepthelement.py +++ b/mango/marketmaking/orderchain/afteraccumulateddepthelement.py @@ -22,7 +22,7 @@ from decimal import Decimal from solana.publickey import PublicKey from .element import Element -from ..modelstate import ModelState +from ...modelstate import ModelState # # πŸ₯­ AfterAccumulatedDepthElement class diff --git a/mango/marketmaking/orderchain/biasquoteonpositionelement.py b/mango/marketmaking/orderchain/biasquoteonpositionelement.py index e234af5..ca0e18b 100644 --- a/mango/marketmaking/orderchain/biasquoteonpositionelement.py +++ b/mango/marketmaking/orderchain/biasquoteonpositionelement.py @@ -20,7 +20,7 @@ import typing from decimal import Decimal from .element import Element -from ..modelstate import ModelState +from ...modelstate import ModelState # # πŸ₯­ BiasQuoteOnPositionElement class diff --git a/mango/marketmaking/orderchain/chain.py b/mango/marketmaking/orderchain/chain.py index 9763feb..4e31748 100644 --- a/mango/marketmaking/orderchain/chain.py +++ b/mango/marketmaking/orderchain/chain.py @@ -18,8 +18,8 @@ import logging import mango import typing -from ..modelstate import ModelState from .element import Element +from ...modelstate import ModelState # # πŸ₯­ Chain class diff --git a/mango/marketmaking/orderchain/confidenceintervalelement.py b/mango/marketmaking/orderchain/confidenceintervalelement.py index 779a64c..158514a 100644 --- a/mango/marketmaking/orderchain/confidenceintervalelement.py +++ b/mango/marketmaking/orderchain/confidenceintervalelement.py @@ -20,7 +20,7 @@ import typing from decimal import Decimal from .element import Element -from ..modelstate import ModelState +from ...modelstate import ModelState # # πŸ₯­ ConfidenceIntervalElement class diff --git a/mango/marketmaking/orderchain/element.py b/mango/marketmaking/orderchain/element.py index 97cb7ae..21c4b6e 100644 --- a/mango/marketmaking/orderchain/element.py +++ b/mango/marketmaking/orderchain/element.py @@ -20,7 +20,7 @@ import logging import mango import typing -from ..modelstate import ModelState +from ...modelstate import ModelState # # πŸ₯­ Element class diff --git a/mango/marketmaking/orderchain/fixedpositionsizeelement.py b/mango/marketmaking/orderchain/fixedpositionsizeelement.py index 5f92830..8048294 100644 --- a/mango/marketmaking/orderchain/fixedpositionsizeelement.py +++ b/mango/marketmaking/orderchain/fixedpositionsizeelement.py @@ -20,7 +20,7 @@ import typing from decimal import Decimal from .element import Element -from ..modelstate import ModelState +from ...modelstate import ModelState # # πŸ₯­ FixedPositionSizeElement class diff --git a/mango/marketmaking/orderchain/fixedspreadelement.py b/mango/marketmaking/orderchain/fixedspreadelement.py index ead66ed..7ad01db 100644 --- a/mango/marketmaking/orderchain/fixedspreadelement.py +++ b/mango/marketmaking/orderchain/fixedspreadelement.py @@ -20,7 +20,7 @@ import typing from decimal import Decimal from .element import Element -from ..modelstate import ModelState +from ...modelstate import ModelState # # πŸ₯­ FixedSpreadElement class diff --git a/mango/marketmaking/orderchain/minimumchargeelement.py b/mango/marketmaking/orderchain/minimumchargeelement.py index 2abe6e3..5391b41 100644 --- a/mango/marketmaking/orderchain/minimumchargeelement.py +++ b/mango/marketmaking/orderchain/minimumchargeelement.py @@ -20,7 +20,7 @@ import typing from decimal import Decimal from .element import Element -from ..modelstate import ModelState +from ...modelstate import ModelState # # πŸ₯­ MinimumChargeElement class diff --git a/mango/marketmaking/orderchain/preventpostonlycrossingbookelement.py b/mango/marketmaking/orderchain/preventpostonlycrossingbookelement.py index 56d05d3..083aaa5 100644 --- a/mango/marketmaking/orderchain/preventpostonlycrossingbookelement.py +++ b/mango/marketmaking/orderchain/preventpostonlycrossingbookelement.py @@ -20,7 +20,7 @@ import typing from decimal import Decimal from .element import Element -from ..modelstate import ModelState +from ...modelstate import ModelState # # πŸ₯­ PreventPostOnlyCrossingBookElement class diff --git a/mango/marketmaking/orderchain/ratioselement.py b/mango/marketmaking/orderchain/ratioselement.py index f981ec0..d936cc9 100644 --- a/mango/marketmaking/orderchain/ratioselement.py +++ b/mango/marketmaking/orderchain/ratioselement.py @@ -20,7 +20,7 @@ import typing from decimal import Decimal from .element import Element -from ..modelstate import ModelState +from ...modelstate import ModelState DEFAULT_SPREAD_RATIO = Decimal("0.01") diff --git a/mango/marketmaking/orderchain/roundtolotsizeelement.py b/mango/marketmaking/orderchain/roundtolotsizeelement.py index 981c773..55c7a74 100644 --- a/mango/marketmaking/orderchain/roundtolotsizeelement.py +++ b/mango/marketmaking/orderchain/roundtolotsizeelement.py @@ -20,7 +20,7 @@ import typing from decimal import Decimal from .element import Element -from ..modelstate import ModelState +from ...modelstate import ModelState # # πŸ₯­ RoundToLotSizeElement class diff --git a/mango/marketmaking/orderreconciler.py b/mango/marketmaking/orderreconciler.py index cfde244..86de1e6 100644 --- a/mango/marketmaking/orderreconciler.py +++ b/mango/marketmaking/orderreconciler.py @@ -19,7 +19,7 @@ import logging import mango import typing -from .modelstate import ModelState +from ..modelstate import ModelState from .reconciledorders import ReconciledOrders diff --git a/mango/marketmaking/toleranceorderreconciler.py b/mango/marketmaking/toleranceorderreconciler.py index 7156366..e454d86 100644 --- a/mango/marketmaking/toleranceorderreconciler.py +++ b/mango/marketmaking/toleranceorderreconciler.py @@ -19,7 +19,7 @@ import typing from decimal import Decimal -from .modelstate import ModelState +from ..modelstate import ModelState from .orderreconciler import OrderReconciler from .reconciledorders import ReconciledOrders diff --git a/mango/marketmaking/modelstate.py b/mango/modelstate.py similarity index 66% rename from mango/marketmaking/modelstate.py rename to mango/modelstate.py index 4873e00..3011f06 100644 --- a/mango/marketmaking/modelstate.py +++ b/mango/modelstate.py @@ -15,13 +15,20 @@ import logging -import mango import typing from decimal import Decimal - from solana.publickey import PublicKey +from .account import Account +from .group import Group +from .inventory import Inventory +from .market import Market +from .oracle import Price +from .orders import Order +from .placedorder import PlacedOrdersContainer +from .watcher import Watcher + # # πŸ₯­ ModelState class # @@ -30,62 +37,61 @@ from solana.publickey import PublicKey class ModelState: def __init__(self, order_owner: PublicKey, - market: mango.Market, - group_watcher: mango.Watcher[mango.Group], - account_watcher: mango.Watcher[mango.Account], - price_watcher: mango.Watcher[mango.Price], - placed_orders_container_watcher: mango.Watcher[mango.PlacedOrdersContainer], - inventory_watcher: mango.Watcher[mango.Inventory], - bids: mango.Watcher[typing.Sequence[mango.Order]], - asks: mango.Watcher[typing.Sequence[mango.Order]] + market: Market, + group_watcher: Watcher[Group], + account_watcher: Watcher[Account], + price_watcher: Watcher[Price], + placed_orders_container_watcher: Watcher[PlacedOrdersContainer], + inventory_watcher: Watcher[Inventory], + bids: Watcher[typing.Sequence[Order]], + asks: Watcher[typing.Sequence[Order]] ): self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.order_owner: PublicKey = order_owner - self.market: mango.Market = market - self.group_watcher: mango.Watcher[mango.Group] = group_watcher - self.account_watcher: mango.Watcher[mango.Account] = account_watcher - self.price_watcher: mango.Watcher[mango.Price] = price_watcher - self.placed_orders_container_watcher: mango.Watcher[ - mango.PlacedOrdersContainer] = placed_orders_container_watcher - self.inventory_watcher: mango.Watcher[ - mango.Inventory] = inventory_watcher - self.bids_watcher: mango.Watcher[typing.Sequence[mango.Order]] = bids - self.asks_watcher: mango.Watcher[typing.Sequence[mango.Order]] = asks + self.market: Market = market + self.group_watcher: Watcher[Group] = group_watcher + self.account_watcher: Watcher[Account] = account_watcher + self.price_watcher: Watcher[Price] = price_watcher + self.placed_orders_container_watcher: Watcher[ + PlacedOrdersContainer] = placed_orders_container_watcher + self.inventory_watcher: Watcher[Inventory] = inventory_watcher + self.bids_watcher: Watcher[typing.Sequence[Order]] = bids + self.asks_watcher: Watcher[typing.Sequence[Order]] = asks self.not_quoting: bool = False self.state: typing.Dict[str, typing.Any] = {} @property - def group(self) -> mango.Group: + def group(self) -> Group: return self.group_watcher.latest @property - def account(self) -> mango.Account: + def account(self) -> Account: return self.account_watcher.latest @property - def price(self) -> mango.Price: + def price(self) -> Price: return self.price_watcher.latest @property - def placed_orders_container(self) -> mango.PlacedOrdersContainer: + def placed_orders_container(self) -> PlacedOrdersContainer: return self.placed_orders_container_watcher.latest @property - def inventory(self) -> mango.Inventory: + def inventory(self) -> Inventory: return self.inventory_watcher.latest @property - def bids(self) -> typing.Sequence[mango.Order]: + def bids(self) -> typing.Sequence[Order]: return self.bids_watcher.latest @property - def asks(self) -> typing.Sequence[mango.Order]: + def asks(self) -> typing.Sequence[Order]: return self.asks_watcher.latest # The top bid is the highest price someone is willing to pay to BUY @property - def top_bid(self) -> typing.Optional[mango.Order]: + def top_bid(self) -> typing.Optional[Order]: if self.bids_watcher.latest and len(self.bids_watcher.latest) > 0: # Top-of-book is always at index 0 for us. return self.bids_watcher.latest[0] @@ -94,7 +100,7 @@ class ModelState: # The top ask is the lowest price someone is willing to pay to SELL @property - def top_ask(self) -> typing.Optional[mango.Order]: + def top_ask(self) -> typing.Optional[Order]: if self.asks_watcher.latest and len(self.asks_watcher.latest) > 0: # Top-of-book is always at index 0 for us. return self.asks_watcher.latest[0] @@ -110,7 +116,7 @@ class ModelState: else: return top_ask.price - top_bid.price - def current_orders(self) -> typing.Sequence[mango.Order]: + def current_orders(self) -> typing.Sequence[Order]: all_orders = [*self.bids_watcher.latest, *self.asks_watcher.latest] return list([o for o in all_orders if o.owner == self.order_owner]) diff --git a/mango/perpaccount.py b/mango/perpaccount.py index c30f3ae..82ec9fc 100644 --- a/mango/perpaccount.py +++ b/mango/perpaccount.py @@ -17,6 +17,8 @@ import typing from decimal import Decimal +from .cache import PerpMarketCache +from .lotsizeconverter import LotSizeConverter from .perpopenorders import PerpOpenOrders from .token import Token from .tokenvalue import TokenValue @@ -30,7 +32,8 @@ class PerpAccount: def __init__(self, base_position: Decimal, quote_position: Decimal, long_settled_funding: Decimal, short_settled_funding: Decimal, bids_quantity: Decimal, asks_quantity: Decimal, taker_base: Decimal, taker_quote: Decimal, mngo_accrued: TokenValue, - open_orders: PerpOpenOrders): + open_orders: PerpOpenOrders, lot_size_converter: LotSizeConverter, + base_token_value: TokenValue): self.base_position: Decimal = base_position self.quote_position: Decimal = quote_position self.long_settled_funding: Decimal = long_settled_funding @@ -41,9 +44,11 @@ class PerpAccount: self.taker_quote: Decimal = taker_quote self.mngo_accrued: TokenValue = mngo_accrued self.open_orders: PerpOpenOrders = open_orders + self.lot_size_converter: LotSizeConverter = lot_size_converter + self.base_token_value: TokenValue = base_token_value @staticmethod - def from_layout(layout: typing.Any, open_orders: PerpOpenOrders, mngo_token: Token) -> "PerpAccount": + def from_layout(layout: typing.Any, base_token: Token, quote_token: Token, open_orders: PerpOpenOrders, lot_size_converter: LotSizeConverter, mngo_token: Token) -> "PerpAccount": base_position: Decimal = layout.base_position quote_position: Decimal = layout.quote_position long_settled_funding: Decimal = layout.long_settled_funding @@ -55,15 +60,63 @@ class PerpAccount: mngo_accrued_raw: Decimal = layout.mngo_accrued mngo_accrued: TokenValue = TokenValue(mngo_token, mngo_token.shift_to_decimals(mngo_accrued_raw)) + base_position_raw = (base_position + taker_base) * lot_size_converter.base_lot_size + base_token_value: TokenValue = TokenValue(base_token, base_token.shift_to_decimals(base_position_raw)) + return PerpAccount(base_position, quote_position, long_settled_funding, short_settled_funding, - bids_quantity, asks_quantity, taker_base, taker_quote, mngo_accrued, open_orders) + bids_quantity, asks_quantity, taker_base, taker_quote, mngo_accrued, open_orders, + lot_size_converter, base_token_value) + + @property + def empty(self) -> bool: + if self.base_position == Decimal(0) and self.quote_position == Decimal(0) and self.long_settled_funding == Decimal(0) and self.short_settled_funding == Decimal(0) and self.mngo_accrued.value == Decimal(0) and self.open_orders.empty: + return True + return False + + def unsettled_funding(self, perp_market_cache: PerpMarketCache) -> Decimal: + if self.base_position < 0: + return self.base_position * (perp_market_cache.short_funding - self.short_settled_funding) + else: + return self.base_position * (perp_market_cache.long_funding - self.long_settled_funding) + + def asset_value(self, perp_market_cache: PerpMarketCache, price: Decimal) -> Decimal: + value: Decimal = Decimal(0) + if self.base_position > 0: + value = self.base_position * self.lot_size_converter.base_lot_size * price + + quote_position: Decimal = self.quote_position + if self.base_position > 0: + quote_position -= (perp_market_cache.long_funding - self.long_settled_funding) * self.base_position + elif self.base_position < 0: + quote_position -= (perp_market_cache.short_funding - self.short_settled_funding) * self.base_position + + if quote_position > 0: + value += quote_position + + return self.lot_size_converter.quote.shift_to_decimals(value) + + def liability_value(self, perp_market_cache: PerpMarketCache, price: Decimal) -> Decimal: + value: Decimal = Decimal(0) + if self.base_position < 0: + value = self.base_position * self.lot_size_converter.base_lot_size * price + + quote_position: Decimal = self.quote_position + if self.base_position > 0: + quote_position -= (perp_market_cache.long_funding - self.long_settled_funding) * self.base_position + elif self.base_position < 0: + quote_position -= (perp_market_cache.short_funding - self.short_settled_funding) * self.base_position + + if quote_position < 0: + value += quote_position + + return self.lot_size_converter.quote.shift_to_decimals(-value) def __str__(self) -> str: - if self.base_position == Decimal(0) and self.quote_position == Decimal(0) and self.long_settled_funding == Decimal(0) and self.short_settled_funding == Decimal(0) and self.mngo_accrued.value == Decimal(0) and self.open_orders.empty: + if self.empty: return "Β« π™ΏπšŽπš›πš™π™°πšŒπšŒπš˜πšžπš—πš (empty) Β»" open_orders = f"{self.open_orders}".replace("\n", "\n ") return f"""Β« π™ΏπšŽπš›πš™π™°πšŒπšŒπš˜πšžπš—πš - Base Position: {self.base_position} + Base Position: {self.base_token_value} Quote Position: {self.quote_position} Long Settled Funding: {self.long_settled_funding} Short Settled Funding: {self.short_settled_funding} diff --git a/tests/fakes.py b/tests/fakes.py index f8cf451..66b3d19 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -159,7 +159,7 @@ def fake_model_state(order_owner: typing.Optional[PublicKey] = None, placed_orders_container: typing.Optional[mango.PlacedOrdersContainer] = None, inventory: typing.Optional[mango.Inventory] = None, bids: typing.Optional[typing.Sequence[mango.Order]] = None, - asks: typing.Optional[typing.Sequence[mango.Order]] = None) -> mango.marketmaking.ModelState: + asks: typing.Optional[typing.Sequence[mango.Order]] = None) -> mango.ModelState: order_owner = order_owner or fake_seeded_public_key("order owner") market = market or fake_loaded_market() group = group or fake_group() @@ -178,6 +178,6 @@ def fake_model_state(order_owner: typing.Optional[PublicKey] = None, bids_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(bids) asks_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(asks) - return mango.marketmaking.ModelState(order_owner, market, group_watcher, - account_watcher, price_watcher, placed_orders_container_watcher, - inventory_watcher, bids_watcher, asks_watcher) + return mango.ModelState(order_owner, market, group_watcher, + account_watcher, price_watcher, placed_orders_container_watcher, + inventory_watcher, bids_watcher, asks_watcher)