Simplified and improved marketmaker hedging.
This commit is contained in:
parent
2f11f99b18
commit
26aabd23e9
121
bin/hedger
121
bin/hedger
|
@ -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.")
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .hedger import Hedger
|
||||||
|
from .nullhedger import NullHedger
|
||||||
|
from .perptospothedger import PerpToSpotHedger
|
|
@ -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}"
|
|
@ -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}"
|
|
@ -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}' »"
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue