Simplified and improved marketmaker hedging.

This commit is contained in:
Geoff Taylor 2021-10-02 12:28:16 +01:00
parent 2f11f99b18
commit 26aabd23e9
30 changed files with 345 additions and 237 deletions

View File

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

View File

@ -10,12 +10,12 @@ import sys
import threading import threading
from decimal import Decimal from decimal import Decimal
from rx.core.abc.disposable import Disposable
from solana.publickey import PublicKey from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath( sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), ".."))) os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8 import mango # nopep8
import mango.hedging # nopep8
import mango.marketmaking # nopep8 import mango.marketmaking # nopep8
from mango.marketmaking.orderchain import chain # nopep8 from mango.marketmaking.orderchain import chain # nopep8
from mango.marketmaking.orderchain import chainbuilder # 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) cleanup(context, wallet, account, market, args.dry_run)
hedger: mango.hedging.Hedger = mango.hedging.NullHedger()
if args.hedge_market is not None: if args.hedge_market is not None:
if not isinstance(market, mango.PerpMarket): if not isinstance(market, mango.PerpMarket):
raise Exception(f"Cannot hedge - market {market.symbol} is not a perp market.") 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_symbol = args.hedge_market.upper()
hedging_market_stub = context.market_lookup.find_by_symbol(hedging_market_symbol) 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( hedging_market_operations: mango.MarketOperations = mango.create_market_operations(
context, wallet, account, hedging_market, args.dry_run) context, wallet, account, hedging_market, args.dry_run)
perp_event_queue: mango.PerpEventQueue = mango.PerpEventQueue.load( hedger = mango.hedging.PerpToSpotHedger(group, underlying_market, hedging_market,
context, watched_market.underlying_perp_market.event_queue, watched_market.lot_size_converter) hedging_market_operations, args.max_price_slippage_factor)
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)
order_reconciler = mango.marketmaking.ToleranceOrderReconciler( 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() 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( pulse_disposable = rx.interval(args.pulse_interval).pipe(
rx.operators.observe_on(context.create_thread_pool_scheduler()), rx.operators.observe_on(context.create_thread_pool_scheduler()),
rx.operators.start_with(-1), rx.operators.start_with(-1),
rx.operators.catch(mango.observable_pipeline_error_reporter), rx.operators.catch(mango.observable_pipeline_error_reporter),
rx.operators.retry() rx.operators.retry()
).subscribe( ).subscribe(
on_next=lambda _: market_maker.pulse(context, model_state_builder.build(context))) on_next=pulse_action)
disposer.add_disposable(pulse_disposable) disposer.add_disposable(pulse_disposable)
# Wait - don't exit. Exiting will be handled by signals/interrupts. # Wait - don't exit. Exiting will be handled by signals/interrupts.

View File

@ -6,6 +6,7 @@ import os.path
import sys import sys
import typing import typing
from decimal import Decimal
from solana.publickey import PublicKey from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath( sys.path.insert(0, os.path.abspath(
@ -15,11 +16,16 @@ import mango # nopep8
def report_accrued(basket_token: mango.AccountBasketBaseToken): def report_accrued(basket_token: mango.AccountBasketBaseToken):
symbol: str = basket_token.token_info.token.symbol 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}") print(f"Accrued in perp market [{symbol:>5}]: {accrued}")
def load_perp_market(context: mango.Context, group: mango.Group, group_basket_market: mango.GroupBasketMarket): 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_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, perp_market = mango.PerpMarket(context.mango_program_address, group_basket_market.perp_market_info.address,
group_basket_market.base_token_info.token, group_basket_market.base_token_info.token,

View File

@ -37,6 +37,7 @@ from .marketinstructionbuilder import MarketInstructionBuilder, NullMarketInstru
from .marketlookup import MarketLookup, NullMarketLookup, CompoundMarketLookup from .marketlookup import MarketLookup, NullMarketLookup, CompoundMarketLookup
from .marketoperations import MarketOperations, DryRunMarketOperations from .marketoperations import MarketOperations, DryRunMarketOperations
from .metadata import Metadata from .metadata import Metadata
from .modelstate import ModelState
from .notification import NotificationTarget, TelegramNotificationTarget, DiscordNotificationTarget, MailjetNotificationTarget, CsvFileNotificationTarget, FilteringNotificationTarget, NotificationHandler, parse_subscription_target 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 .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 from .openorders import OpenOrders

View File

@ -137,6 +137,10 @@ class Account(AddressableAccount):
placed_order = PlacedOrder(id, client_id, side) placed_order = PlacedOrder(id, client_id, side)
placed_orders_all_markets[int(order_market)] += [placed_order] 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]): for index, token_info in enumerate(group.tokens[:-1]):
if token_info: if token_info:
intrinsic_deposit = token_info.root_bank.deposit_index * layout.deposits[index] 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] intrinsic_borrow = token_info.root_bank.borrow_index * layout.borrows[index]
borrow = TokenValue(token_info.token, token_info.token.shift_to_decimals(intrinsic_borrow)) borrow = TokenValue(token_info.token, token_info.token.shift_to_decimals(intrinsic_borrow))
perp_open_orders = PerpOpenOrders(placed_orders_all_markets[index]) 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( 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] spot_open_orders = layout.spot_open_orders[index]
basket_item: AccountBasketBaseToken = AccountBasketBaseToken( basket_item: AccountBasketBaseToken = AccountBasketBaseToken(
token_info, deposit, borrow, spot_open_orders, perp_account) token_info, deposit, borrow, spot_open_orders, perp_account)
@ -154,10 +166,6 @@ class Account(AddressableAccount):
else: else:
active_in_basket += [False] 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] intrinsic_quote_deposit = quote_token_info.root_bank.deposit_index * layout.deposits[-1]
quote_deposit = TokenValue(quote_token_info.token, quote_deposit = TokenValue(quote_token_info.token,
quote_token_info.token.shift_to_decimals(intrinsic_quote_deposit)) quote_token_info.token.shift_to_decimals(intrinsic_quote_deposit))

View File

@ -22,6 +22,7 @@ from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount from .addressableaccount import AddressableAccount
from .context import Context from .context import Context
from .layouts import layouts from .layouts import layouts
from .lotsizeconverter import LotSizeConverter, RaisingLotSizeConverter
from .marketlookup import MarketLookup from .marketlookup import MarketLookup
from .metadata import Metadata from .metadata import Metadata
from .perpmarketinfo import PerpMarketInfo from .perpmarketinfo import PerpMarketInfo
@ -39,11 +40,12 @@ from .version import Version
# `GroupBasketMarket` gathers basket items together instead of separate arrays. # `GroupBasketMarket` gathers basket items together instead of separate arrays.
# #
class GroupBasketMarket: 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.base_token_info: TokenInfo = base_token_info
self.quote_token_info: TokenInfo = quote_token_info self.quote_token_info: TokenInfo = quote_token_info
self.spot_market_info: SpotMarketInfo = spot_market_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 self.oracle: PublicKey = oracle
def __str__(self) -> str: def __str__(self) -> str:
@ -141,11 +143,24 @@ class Group(AddressableAccount):
in_basket: typing.List[bool] = [] in_basket: typing.List[bool] = []
for index, base_token_info in enumerate(tokens[:-1]): for index, base_token_info in enumerate(tokens[:-1]):
if base_token_info is not None: if base_token_info is not None:
spot_market_info: SpotMarketInfo = SpotMarketInfo.from_layout(layout.spot_markets[index]) spot_market_info: typing.Optional[SpotMarketInfo] = SpotMarketInfo.from_layout_or_none(
perp_market_info: PerpMarketInfo = PerpMarketInfo.from_layout(layout.perp_markets[index]) 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] oracle: PublicKey = layout.oracles[index]
item: GroupBasketMarket = GroupBasketMarket( 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] basket += [item]
in_basket += [True] in_basket += [True]
else: else:

View File

@ -0,0 +1,3 @@
from .hedger import Hedger
from .nullhedger import NullHedger
from .perptospothedger import PerpToSpotHedger

42
mango/hedging/hedger.py Normal file
View File

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

View File

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

View File

@ -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}' »"

View File

@ -1,5 +1,4 @@
from .marketmaker import MarketMaker from .marketmaker import MarketMaker
from .modelstate import ModelState
from .modelstatebuilder import ModelStateBuilder, WebsocketModelStateBuilder, PollingModelStateBuilder, SerumPollingModelStateBuilder, SpotPollingModelStateBuilder, PerpPollingModelStateBuilder from .modelstatebuilder import ModelStateBuilder, WebsocketModelStateBuilder, PollingModelStateBuilder, SerumPollingModelStateBuilder, SpotPollingModelStateBuilder, PerpPollingModelStateBuilder
from .modelstatebuilderfactory import ModelUpdateMode, model_state_builder_factory from .modelstatebuilderfactory import ModelUpdateMode, model_state_builder_factory
from .orderreconciler import OrderReconciler, NullOrderReconciler from .orderreconciler import OrderReconciler, NullOrderReconciler

View File

@ -22,7 +22,6 @@ import typing
from datetime import datetime from datetime import datetime
from decimal import Decimal from decimal import Decimal
from .modelstate import ModelState
from ..observables import EventSource from ..observables import EventSource
from .orderreconciler import OrderReconciler from .orderreconciler import OrderReconciler
from .orderchain.chain import Chain from .orderchain.chain import Chain
@ -51,7 +50,7 @@ class MarketMaker:
self.buy_client_ids: typing.List[int] = [] self.buy_client_ids: typing.List[int] = []
self.sell_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: try:
payer = mango.CombinableInstructions.from_wallet(self.wallet) payer = mango.CombinableInstructions.from_wallet(self.wallet)

View File

@ -22,7 +22,7 @@ import typing
from decimal import Decimal from decimal import Decimal
from solana.publickey import PublicKey from solana.publickey import PublicKey
from .modelstate import ModelState from ..modelstate import ModelState
from ..tokenvalue import TokenValue from ..tokenvalue import TokenValue

View File

@ -20,7 +20,7 @@ import typing
from solana.publickey import PublicKey from solana.publickey import PublicKey
from ..constants import SYSTEM_PROGRAM_ADDRESS from ..constants import SYSTEM_PROGRAM_ADDRESS
from .modelstate import ModelState from ..modelstate import ModelState
from .modelstatebuilder import ModelStateBuilder, WebsocketModelStateBuilder, SerumPollingModelStateBuilder, SpotPollingModelStateBuilder, PerpPollingModelStateBuilder from .modelstatebuilder import ModelStateBuilder, WebsocketModelStateBuilder, SerumPollingModelStateBuilder, SpotPollingModelStateBuilder, PerpPollingModelStateBuilder

View File

@ -22,7 +22,7 @@ from decimal import Decimal
from solana.publickey import PublicKey from solana.publickey import PublicKey
from .element import Element from .element import Element
from ..modelstate import ModelState from ...modelstate import ModelState
# # 🥭 AfterAccumulatedDepthElement class # # 🥭 AfterAccumulatedDepthElement class

View File

@ -20,7 +20,7 @@ import typing
from decimal import Decimal from decimal import Decimal
from .element import Element from .element import Element
from ..modelstate import ModelState from ...modelstate import ModelState
# # 🥭 BiasQuoteOnPositionElement class # # 🥭 BiasQuoteOnPositionElement class

View File

@ -18,8 +18,8 @@ import logging
import mango import mango
import typing import typing
from ..modelstate import ModelState
from .element import Element from .element import Element
from ...modelstate import ModelState
# # 🥭 Chain class # # 🥭 Chain class

View File

@ -20,7 +20,7 @@ import typing
from decimal import Decimal from decimal import Decimal
from .element import Element from .element import Element
from ..modelstate import ModelState from ...modelstate import ModelState
# # 🥭 ConfidenceIntervalElement class # # 🥭 ConfidenceIntervalElement class

View File

@ -20,7 +20,7 @@ import logging
import mango import mango
import typing import typing
from ..modelstate import ModelState from ...modelstate import ModelState
# # 🥭 Element class # # 🥭 Element class

View File

@ -20,7 +20,7 @@ import typing
from decimal import Decimal from decimal import Decimal
from .element import Element from .element import Element
from ..modelstate import ModelState from ...modelstate import ModelState
# # 🥭 FixedPositionSizeElement class # # 🥭 FixedPositionSizeElement class

View File

@ -20,7 +20,7 @@ import typing
from decimal import Decimal from decimal import Decimal
from .element import Element from .element import Element
from ..modelstate import ModelState from ...modelstate import ModelState
# # 🥭 FixedSpreadElement class # # 🥭 FixedSpreadElement class

View File

@ -20,7 +20,7 @@ import typing
from decimal import Decimal from decimal import Decimal
from .element import Element from .element import Element
from ..modelstate import ModelState from ...modelstate import ModelState
# # 🥭 MinimumChargeElement class # # 🥭 MinimumChargeElement class

View File

@ -20,7 +20,7 @@ import typing
from decimal import Decimal from decimal import Decimal
from .element import Element from .element import Element
from ..modelstate import ModelState from ...modelstate import ModelState
# # 🥭 PreventPostOnlyCrossingBookElement class # # 🥭 PreventPostOnlyCrossingBookElement class

View File

@ -20,7 +20,7 @@ import typing
from decimal import Decimal from decimal import Decimal
from .element import Element from .element import Element
from ..modelstate import ModelState from ...modelstate import ModelState
DEFAULT_SPREAD_RATIO = Decimal("0.01") DEFAULT_SPREAD_RATIO = Decimal("0.01")

View File

@ -20,7 +20,7 @@ import typing
from decimal import Decimal from decimal import Decimal
from .element import Element from .element import Element
from ..modelstate import ModelState from ...modelstate import ModelState
# # 🥭 RoundToLotSizeElement class # # 🥭 RoundToLotSizeElement class

View File

@ -19,7 +19,7 @@ import logging
import mango import mango
import typing import typing
from .modelstate import ModelState from ..modelstate import ModelState
from .reconciledorders import ReconciledOrders from .reconciledorders import ReconciledOrders

View File

@ -19,7 +19,7 @@ import typing
from decimal import Decimal from decimal import Decimal
from .modelstate import ModelState from ..modelstate import ModelState
from .orderreconciler import OrderReconciler from .orderreconciler import OrderReconciler
from .reconciledorders import ReconciledOrders from .reconciledorders import ReconciledOrders

View File

@ -15,13 +15,20 @@
import logging import logging
import mango
import typing import typing
from decimal import Decimal from decimal import Decimal
from solana.publickey import PublicKey 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 # # 🥭 ModelState class
# #
@ -30,62 +37,61 @@ from solana.publickey import PublicKey
class ModelState: class ModelState:
def __init__(self, def __init__(self,
order_owner: PublicKey, order_owner: PublicKey,
market: mango.Market, market: Market,
group_watcher: mango.Watcher[mango.Group], group_watcher: Watcher[Group],
account_watcher: mango.Watcher[mango.Account], account_watcher: Watcher[Account],
price_watcher: mango.Watcher[mango.Price], price_watcher: Watcher[Price],
placed_orders_container_watcher: mango.Watcher[mango.PlacedOrdersContainer], placed_orders_container_watcher: Watcher[PlacedOrdersContainer],
inventory_watcher: mango.Watcher[mango.Inventory], inventory_watcher: Watcher[Inventory],
bids: mango.Watcher[typing.Sequence[mango.Order]], bids: Watcher[typing.Sequence[Order]],
asks: mango.Watcher[typing.Sequence[mango.Order]] asks: Watcher[typing.Sequence[Order]]
): ):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.order_owner: PublicKey = order_owner self.order_owner: PublicKey = order_owner
self.market: mango.Market = market self.market: Market = market
self.group_watcher: mango.Watcher[mango.Group] = group_watcher self.group_watcher: Watcher[Group] = group_watcher
self.account_watcher: mango.Watcher[mango.Account] = account_watcher self.account_watcher: Watcher[Account] = account_watcher
self.price_watcher: mango.Watcher[mango.Price] = price_watcher self.price_watcher: Watcher[Price] = price_watcher
self.placed_orders_container_watcher: mango.Watcher[ self.placed_orders_container_watcher: Watcher[
mango.PlacedOrdersContainer] = placed_orders_container_watcher PlacedOrdersContainer] = placed_orders_container_watcher
self.inventory_watcher: mango.Watcher[ self.inventory_watcher: Watcher[Inventory] = inventory_watcher
mango.Inventory] = inventory_watcher self.bids_watcher: Watcher[typing.Sequence[Order]] = bids
self.bids_watcher: mango.Watcher[typing.Sequence[mango.Order]] = bids self.asks_watcher: Watcher[typing.Sequence[Order]] = asks
self.asks_watcher: mango.Watcher[typing.Sequence[mango.Order]] = asks
self.not_quoting: bool = False self.not_quoting: bool = False
self.state: typing.Dict[str, typing.Any] = {} self.state: typing.Dict[str, typing.Any] = {}
@property @property
def group(self) -> mango.Group: def group(self) -> Group:
return self.group_watcher.latest return self.group_watcher.latest
@property @property
def account(self) -> mango.Account: def account(self) -> Account:
return self.account_watcher.latest return self.account_watcher.latest
@property @property
def price(self) -> mango.Price: def price(self) -> Price:
return self.price_watcher.latest return self.price_watcher.latest
@property @property
def placed_orders_container(self) -> mango.PlacedOrdersContainer: def placed_orders_container(self) -> PlacedOrdersContainer:
return self.placed_orders_container_watcher.latest return self.placed_orders_container_watcher.latest
@property @property
def inventory(self) -> mango.Inventory: def inventory(self) -> Inventory:
return self.inventory_watcher.latest return self.inventory_watcher.latest
@property @property
def bids(self) -> typing.Sequence[mango.Order]: def bids(self) -> typing.Sequence[Order]:
return self.bids_watcher.latest return self.bids_watcher.latest
@property @property
def asks(self) -> typing.Sequence[mango.Order]: def asks(self) -> typing.Sequence[Order]:
return self.asks_watcher.latest return self.asks_watcher.latest
# The top bid is the highest price someone is willing to pay to BUY # The top bid is the highest price someone is willing to pay to BUY
@property @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: if self.bids_watcher.latest and len(self.bids_watcher.latest) > 0:
# Top-of-book is always at index 0 for us. # Top-of-book is always at index 0 for us.
return self.bids_watcher.latest[0] 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 # The top ask is the lowest price someone is willing to pay to SELL
@property @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: if self.asks_watcher.latest and len(self.asks_watcher.latest) > 0:
# Top-of-book is always at index 0 for us. # Top-of-book is always at index 0 for us.
return self.asks_watcher.latest[0] return self.asks_watcher.latest[0]
@ -110,7 +116,7 @@ class ModelState:
else: else:
return top_ask.price - top_bid.price 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] all_orders = [*self.bids_watcher.latest, *self.asks_watcher.latest]
return list([o for o in all_orders if o.owner == self.order_owner]) return list([o for o in all_orders if o.owner == self.order_owner])

View File

@ -17,6 +17,8 @@ import typing
from decimal import Decimal from decimal import Decimal
from .cache import PerpMarketCache
from .lotsizeconverter import LotSizeConverter
from .perpopenorders import PerpOpenOrders from .perpopenorders import PerpOpenOrders
from .token import Token from .token import Token
from .tokenvalue import TokenValue from .tokenvalue import TokenValue
@ -30,7 +32,8 @@ class PerpAccount:
def __init__(self, base_position: Decimal, quote_position: Decimal, long_settled_funding: Decimal, def __init__(self, base_position: Decimal, quote_position: Decimal, long_settled_funding: Decimal,
short_settled_funding: Decimal, bids_quantity: Decimal, asks_quantity: Decimal, short_settled_funding: Decimal, bids_quantity: Decimal, asks_quantity: Decimal,
taker_base: Decimal, taker_quote: Decimal, mngo_accrued: TokenValue, 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.base_position: Decimal = base_position
self.quote_position: Decimal = quote_position self.quote_position: Decimal = quote_position
self.long_settled_funding: Decimal = long_settled_funding self.long_settled_funding: Decimal = long_settled_funding
@ -41,9 +44,11 @@ class PerpAccount:
self.taker_quote: Decimal = taker_quote self.taker_quote: Decimal = taker_quote
self.mngo_accrued: TokenValue = mngo_accrued self.mngo_accrued: TokenValue = mngo_accrued
self.open_orders: PerpOpenOrders = open_orders self.open_orders: PerpOpenOrders = open_orders
self.lot_size_converter: LotSizeConverter = lot_size_converter
self.base_token_value: TokenValue = base_token_value
@staticmethod @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 base_position: Decimal = layout.base_position
quote_position: Decimal = layout.quote_position quote_position: Decimal = layout.quote_position
long_settled_funding: Decimal = layout.long_settled_funding long_settled_funding: Decimal = layout.long_settled_funding
@ -55,15 +60,63 @@ class PerpAccount:
mngo_accrued_raw: Decimal = layout.mngo_accrued mngo_accrued_raw: Decimal = layout.mngo_accrued
mngo_accrued: TokenValue = TokenValue(mngo_token, mngo_token.shift_to_decimals(mngo_accrued_raw)) 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, 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: 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) »" return "« 𝙿𝚎𝚛𝚙𝙰𝚌𝚌𝚘𝚞𝚗𝚝 (empty) »"
open_orders = f"{self.open_orders}".replace("\n", "\n ") open_orders = f"{self.open_orders}".replace("\n", "\n ")
return f"""« 𝙿𝚎𝚛𝚙𝙰𝚌𝚌𝚘𝚞𝚗𝚝 return f"""« 𝙿𝚎𝚛𝚙𝙰𝚌𝚌𝚘𝚞𝚗𝚝
Base Position: {self.base_position} Base Position: {self.base_token_value}
Quote Position: {self.quote_position} Quote Position: {self.quote_position}
Long Settled Funding: {self.long_settled_funding} Long Settled Funding: {self.long_settled_funding}
Short Settled Funding: {self.short_settled_funding} Short Settled Funding: {self.short_settled_funding}

View File

@ -159,7 +159,7 @@ def fake_model_state(order_owner: typing.Optional[PublicKey] = None,
placed_orders_container: typing.Optional[mango.PlacedOrdersContainer] = None, placed_orders_container: typing.Optional[mango.PlacedOrdersContainer] = None,
inventory: typing.Optional[mango.Inventory] = None, inventory: typing.Optional[mango.Inventory] = None,
bids: typing.Optional[typing.Sequence[mango.Order]] = 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") order_owner = order_owner or fake_seeded_public_key("order owner")
market = market or fake_loaded_market() market = market or fake_loaded_market()
group = group or fake_group() 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) bids_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(bids)
asks_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(asks) asks_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(asks)
return mango.marketmaking.ModelState(order_owner, market, group_watcher, return mango.ModelState(order_owner, market, group_watcher,
account_watcher, price_watcher, placed_orders_container_watcher, account_watcher, price_watcher, placed_orders_container_watcher,
inventory_watcher, bids_watcher, asks_watcher) inventory_watcher, bids_watcher, asks_watcher)