Added hedger command. Had to introduce a LotSizeConverter - may expand its use now it's available.
This commit is contained in:
parent
373392cf78
commit
ab3657958d
|
@ -0,0 +1,133 @@
|
|||
#!/usr/bin/env pyston3
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import rx
|
||||
import rx.operators
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
sys.path.insert(0, os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..")))
|
||||
import mango # nopep8
|
||||
import mango.layouts # 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("--market", type=str, required=True, help="perp market symbol to hedge (e.g. ETH-PERP)")
|
||||
parser.add_argument("--account-index", type=int, default=0,
|
||||
help="index of the account to use, if more than one available")
|
||||
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 (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("--dry-run", action="store_true", default=False,
|
||||
help="runs as read-only and does not perform any transactions")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.getLogger().setLevel(args.log_level)
|
||||
for notify in args.notify_errors:
|
||||
handler = mango.NotificationHandler(notify)
|
||||
handler.setLevel(logging.ERROR)
|
||||
logging.getLogger().addHandler(handler)
|
||||
|
||||
logging.warning(mango.WARNING_DISCLAIMER_TEXT)
|
||||
|
||||
logger: logging.Logger = logging.getLogger("Hedger")
|
||||
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_id)
|
||||
accounts = mango.Account.load_all_for_owner(context, wallet.address, group)
|
||||
if len(accounts) == 0:
|
||||
raise Exception(f"No mango account found for root address '{wallet.address}'.")
|
||||
account = accounts[args.account_index]
|
||||
|
||||
disposer = mango.DisposePropagator()
|
||||
manager = mango.WebSocketSubscriptionManager()
|
||||
disposer.add_disposable(manager)
|
||||
|
||||
buy_price_adjustment_factor: Decimal = Decimal("1") + args.max_price_slippage_factor
|
||||
sell_price_adjustment_factor: Decimal = Decimal("1") - args.max_price_slippage_factor
|
||||
|
||||
watched_market_symbol = args.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 = f"{watched_market.base.symbol}/USDC"
|
||||
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, args.dry_run, hedging_market)
|
||||
|
||||
initial: mango.PerpEventQueue = mango.PerpEventQueue.load(
|
||||
context, watched_market.underlying_perp_market.event_queue, watched_market.lot_size_converter)
|
||||
splitter: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker(initial)
|
||||
|
||||
event_splitting_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_splitting_subscription)
|
||||
publisher = event_splitting_subscription.publisher.pipe(
|
||||
rx.operators.observe_on(context.pool_scheduler),
|
||||
rx.operators.flat_map(splitter.unseen),
|
||||
rx.operators.filter(lambda event: isinstance(event, mango.PerpFillEvent)),
|
||||
rx.operators.filter(lambda event: (event.maker == account.address) or (event.taker == account.address)),
|
||||
rx.operators.catch(mango.observable_pipeline_error_reporter),
|
||||
rx.operators.retry()
|
||||
)
|
||||
|
||||
|
||||
def hedge(event: mango.PerpFillEvent) -> None:
|
||||
if event.maker == event.taker:
|
||||
logger.info(f"Ignoring self-trade of {event.quantity:,.8f} at {event.price:,.8f} on {watched_market.symbol}.")
|
||||
return
|
||||
|
||||
opposite_side: mango.Side = mango.Side.BUY if event.side == mango.Side.SELL else mango.Side.SELL
|
||||
price_adjustment_factor: Decimal = sell_price_adjustment_factor if opposite_side == mango.Side.SELL else buy_price_adjustment_factor
|
||||
adjusted_price: Decimal = event.price * price_adjustment_factor
|
||||
quantity: Decimal = event.quantity
|
||||
order: mango.Order = mango.Order.from_basic_info(opposite_side, adjusted_price, event.quantity, mango.OrderType.IOC)
|
||||
logger.info(f"Hedging {event.side} of {event.quantity:,.8f} at {event.price:,.8f} on {watched_market.symbol} with {opposite_side} of {quantity:,.8f} at {adjusted_price:,.8f} on {hedging_market.symbol}\n\t{order}")
|
||||
try:
|
||||
hedging_market_operations.place_order(order)
|
||||
except Exception as exception:
|
||||
logger.error(f"Failed to hedge using order {order} - {exception} - {traceback.format_exc()}")
|
||||
|
||||
|
||||
publisher.subscribe(on_next=hedge)
|
||||
|
||||
websocket_url = context.cluster_url.replace("https", "ws", 1)
|
||||
ws: mango.ReconnectingWebsocket = mango.ReconnectingWebsocket(websocket_url, manager.open_handler, manager.on_item)
|
||||
ws.ping_interval = 10
|
||||
ws_pong_disposable = ws.pong.subscribe(mango.FileToucherObserver("/var/tmp/mango_healthcheck_ws_pong"))
|
||||
ws.open()
|
||||
|
||||
# Wait - don't exit. Exiting will be handled by signals/interrupts.
|
||||
waiter = threading.Event()
|
||||
try:
|
||||
waiter.wait()
|
||||
except:
|
||||
pass
|
||||
|
||||
logging.info("Shutting down...")
|
||||
ws.close()
|
||||
disposer.dispose()
|
||||
logging.info("Shutdown complete.")
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env pyston3
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..")))
|
||||
import mango # nopep8
|
||||
|
||||
parser = argparse.ArgumentParser(description="Shows all properties of a given market.")
|
||||
mango.ContextBuilder.add_command_line_parameters(parser)
|
||||
parser.add_argument("--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.getLogger().setLevel(args.log_level)
|
||||
logging.warning(mango.WARNING_DISCLAIMER_TEXT)
|
||||
|
||||
context = mango.ContextBuilder.from_command_line_parameters(args)
|
||||
market_symbol = args.market.upper()
|
||||
market = context.market_lookup.find_by_symbol(market_symbol)
|
||||
if market is None:
|
||||
raise Exception(f"Could not find market {market_symbol}")
|
||||
|
||||
actual_market = mango.ensure_market_loaded(context, market)
|
||||
print(actual_market)
|
|
@ -43,10 +43,12 @@ if args.account_type.upper() == "ACCOUNTINFO":
|
|||
manager.add(raw_subscription)
|
||||
publisher: rx.Observable = raw_subscription.publisher
|
||||
elif args.account_type.upper() == "PERPEVENTS":
|
||||
initial: mango.PerpEventQueue = mango.PerpEventQueue.load(context, args.address)
|
||||
# It'd be nice to get the market's lot size converter, but we don't have its address yet.
|
||||
lot_size_converter: mango.LotSizeConverter = mango.NullLotSizeConverter()
|
||||
initial: mango.PerpEventQueue = mango.PerpEventQueue.load(context, args.address, lot_size_converter)
|
||||
splitter: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker(initial)
|
||||
event_splitting_subscription = mango.WebSocketAccountSubscription(
|
||||
context, args.address, mango.PerpEventQueue.parse)
|
||||
context, args.address, lambda account_info: mango.PerpEventQueue.parse(account_info, lot_size_converter))
|
||||
manager.add(event_splitting_subscription)
|
||||
publisher = event_splitting_subscription.publisher.pipe(rx.operators.flat_map(splitter.unseen))
|
||||
else:
|
||||
|
|
|
@ -22,6 +22,7 @@ from .instructiontype import InstructionType
|
|||
from .liquidatablereport import LiquidatableState, LiquidatableReport
|
||||
from .liquidationevent import LiquidationEvent
|
||||
from .liquidationprocessor import LiquidationProcessor, LiquidationProcessorState
|
||||
from .lotsizeconverter import LotSizeConverter, NullLotSizeConverter
|
||||
from .market import InventorySource, Market
|
||||
from .marketinstructionbuilder import MarketInstructionBuilder, NullMarketInstructionBuilder
|
||||
from .marketlookup import MarketLookup, NullMarketLookup, CompoundMarketLookup
|
||||
|
|
|
@ -24,6 +24,7 @@ from .addressableaccount import AddressableAccount
|
|||
from .context import Context
|
||||
from .group import Group
|
||||
from .layouts import layouts
|
||||
from .lotsizeconverter import NullLotSizeConverter
|
||||
from .openorders import OpenOrders
|
||||
from .perpeventqueue import PerpEventQueue
|
||||
from .perpmarketdetails import PerpMarketDetails
|
||||
|
@ -50,7 +51,7 @@ def build_account_info_converter(context: Context, account_type: str) -> typing.
|
|||
elif account_type_upper == "OPENORDERS":
|
||||
return lambda account_info: OpenOrders.parse(account_info, Decimal(6), Decimal(6))
|
||||
elif account_type_upper == "PERPEVENTQUEUE":
|
||||
return lambda account_info: PerpEventQueue.parse(account_info)
|
||||
return lambda account_info: PerpEventQueue.parse(account_info, NullLotSizeConverter())
|
||||
elif account_type_upper == "SERUMEVENTQUEUE":
|
||||
return lambda account_info: SerumEventQueue.parse(account_info)
|
||||
elif account_type_upper == "PERPMARKETDETAILS":
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# # ⚠ 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)
|
||||
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from .token import Token
|
||||
|
||||
# # 🥭 LotSizeConverter class
|
||||
#
|
||||
|
||||
|
||||
class LotSizeConverter():
|
||||
def __init__(self, base: Token, base_lot_size: Decimal, quote: Token, quote_lot_size: Decimal):
|
||||
self.base: Token = base
|
||||
self.base_lot_size: Decimal = base_lot_size
|
||||
self.quote: Token = quote
|
||||
self.quote_lot_size: Decimal = quote_lot_size
|
||||
|
||||
def price_lots_to_native(self, price_lots: Decimal) -> Decimal:
|
||||
return (self.quote_lot_size * price_lots) / self.base_lot_size
|
||||
|
||||
def quantity_lots_to_native(self, quantity_lots: Decimal) -> Decimal:
|
||||
return self.base_lot_size * quantity_lots
|
||||
|
||||
def price_lots_to_value(self, price_lots: Decimal) -> Decimal:
|
||||
native_to_ui: Decimal = 10 ** (self.base.decimals - self.quote.decimals)
|
||||
lots_to_native: Decimal = self.quote_lot_size / self.base_lot_size
|
||||
return (price_lots * lots_to_native) * native_to_ui
|
||||
|
||||
def quantity_lots_to_value(self, quantity_lots: Decimal) -> Decimal:
|
||||
return self.base.shift_to_native(quantity_lots * self.base_lot_size)
|
||||
|
||||
def __str__(self):
|
||||
return f"« 𝙻𝚘𝚝𝚂𝚒𝚣𝚎𝙲𝚘𝚗𝚟𝚎𝚛𝚝𝚎𝚛 [base lot size: {self.base_lot_size}, quote lot size: {self.quote_lot_size}] »"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self}"
|
||||
|
||||
|
||||
# # 🥭 NullLotSizeConverter class
|
||||
#
|
||||
|
||||
|
||||
class NullLotSizeConverter(LotSizeConverter):
|
||||
def __init__(self):
|
||||
super().__init__(Decimal(1), Decimal(1))
|
||||
|
||||
def price_lots_to_native(self, price_lots: Decimal) -> Decimal:
|
||||
return price_lots
|
||||
|
||||
def quantity_lots_to_native(self, quantity_lots: Decimal) -> Decimal:
|
||||
return quantity_lots
|
||||
|
||||
def price_lots_to_value(self, price_lots: Decimal) -> Decimal:
|
||||
return price_lots
|
||||
|
||||
def quantity_lots_to_value(self, quantity_lots: Decimal) -> Decimal:
|
||||
return quantity_lots
|
||||
|
||||
def __str__(self):
|
||||
return "« 𝙽𝚞𝚕𝚕𝙻𝚘𝚝𝚂𝚒𝚣𝚎𝙲𝚘𝚗𝚟𝚎𝚛𝚝𝚎𝚛 »"
|
|
@ -25,6 +25,7 @@ from .accountinfo import AccountInfo
|
|||
from .addressableaccount import AddressableAccount
|
||||
from .context import Context
|
||||
from .layouts import layouts
|
||||
from .lotsizeconverter import LotSizeConverter
|
||||
from .metadata import Metadata
|
||||
from .orders import Side
|
||||
from .version import Version
|
||||
|
@ -130,13 +131,15 @@ class PerpUnknownEvent(PerpEvent):
|
|||
#
|
||||
# `event_builder()` takes an event layout and returns a typed `PerpEvent`.
|
||||
#
|
||||
def event_builder(event_layout) -> typing.Optional[PerpEvent]:
|
||||
def event_builder(lot_size_converter: LotSizeConverter, event_layout) -> typing.Optional[PerpEvent]:
|
||||
if event_layout.event_type == b'\x00':
|
||||
if event_layout.maker is None and event_layout.taker is None:
|
||||
return None
|
||||
side: Side = Side.BUY if event_layout.side == pyserum.enums.Side.BUY else Side.SELL
|
||||
quantity: Decimal = lot_size_converter.quantity_lots_to_value(event_layout.quantity)
|
||||
price: Decimal = lot_size_converter.price_lots_to_value(event_layout.price)
|
||||
return PerpFillEvent(event_layout.event_type, event_layout.timestamp, side,
|
||||
event_layout.price, event_layout.quantity, event_layout.best_initial,
|
||||
price, quantity, event_layout.best_initial,
|
||||
event_layout.maker_slot, event_layout.maker_out, event_layout.maker,
|
||||
event_layout.maker_order_id, event_layout.maker_client_order_id,
|
||||
event_layout.taker, event_layout.taker_order_id,
|
||||
|
@ -166,27 +169,30 @@ class PerpEventQueue(AddressableAccount):
|
|||
self.events: typing.Sequence[typing.Optional[PerpEvent]] = events
|
||||
|
||||
@staticmethod
|
||||
def from_layout(layout: layouts.PERP_EVENT_QUEUE, account_info: AccountInfo, version: Version) -> "PerpEventQueue":
|
||||
def from_layout(layout: layouts.PERP_EVENT_QUEUE, account_info: AccountInfo, version: Version, lot_size_converter: LotSizeConverter) -> "PerpEventQueue":
|
||||
meta_data: Metadata = Metadata.from_layout(layout.meta_data)
|
||||
head: Decimal = layout.head
|
||||
count: Decimal = layout.count
|
||||
seq_num: Decimal = layout.seq_num
|
||||
events: typing.Sequence[typing.Optional[PerpEvent]] = list(map(event_builder, layout.events))
|
||||
events: typing.List[typing.Optional[PerpEvent]] = []
|
||||
for raw_event in layout.events:
|
||||
built_event = event_builder(lot_size_converter, raw_event)
|
||||
events += [built_event]
|
||||
|
||||
return PerpEventQueue(account_info, version, meta_data, head, count, seq_num, events)
|
||||
|
||||
@ staticmethod
|
||||
def parse(account_info: AccountInfo) -> "PerpEventQueue":
|
||||
def parse(account_info: AccountInfo, lot_size_converter: LotSizeConverter) -> "PerpEventQueue":
|
||||
# Data length isn't fixed so can't check we get the right value the way we normally do.
|
||||
layout = layouts.PERP_EVENT_QUEUE.parse(account_info.data)
|
||||
return PerpEventQueue.from_layout(layout, account_info, Version.V1)
|
||||
return PerpEventQueue.from_layout(layout, account_info, Version.V1, lot_size_converter)
|
||||
|
||||
@ staticmethod
|
||||
def load(context: Context, address: PublicKey) -> "PerpEventQueue":
|
||||
def load(context: Context, address: PublicKey, lot_size_converter: LotSizeConverter) -> "PerpEventQueue":
|
||||
account_info = AccountInfo.load(context, address)
|
||||
if account_info is None:
|
||||
raise Exception(f"PerpEventQueue account not found at address '{address}'")
|
||||
return PerpEventQueue.parse(account_info)
|
||||
return PerpEventQueue.parse(account_info, lot_size_converter)
|
||||
|
||||
@property
|
||||
def capacity(self) -> int:
|
||||
|
|
|
@ -20,6 +20,7 @@ from solana.publickey import PublicKey
|
|||
from .accountinfo import AccountInfo
|
||||
from .context import Context
|
||||
from .group import Group
|
||||
from .lotsizeconverter import LotSizeConverter
|
||||
from .market import Market, InventorySource
|
||||
from .orderbookside import OrderBookSide
|
||||
from .orders import Order
|
||||
|
@ -37,6 +38,8 @@ class PerpMarket(Market):
|
|||
def __init__(self, address: PublicKey, base: Token, quote: Token, underlying_perp_market: PerpMarketDetails):
|
||||
super().__init__(address, InventorySource.ACCOUNT, base, quote)
|
||||
self.underlying_perp_market: PerpMarketDetails = underlying_perp_market
|
||||
self.lot_size_converter: LotSizeConverter = LotSizeConverter(
|
||||
base, underlying_perp_market.base_lot_size, quote, underlying_perp_market.quote_lot_size)
|
||||
|
||||
@property
|
||||
def symbol(self) -> str:
|
||||
|
@ -47,7 +50,8 @@ class PerpMarket(Market):
|
|||
return self.underlying_perp_market.group
|
||||
|
||||
def unprocessed_events(self, context: Context) -> typing.Sequence[PerpEvent]:
|
||||
event_queue: PerpEventQueue = PerpEventQueue.load(context, self.underlying_perp_market.event_queue)
|
||||
event_queue: PerpEventQueue = PerpEventQueue.load(
|
||||
context, self.underlying_perp_market.event_queue, self.lot_size_converter)
|
||||
return event_queue.unprocessed_events()
|
||||
|
||||
def accounts_to_crank(self, context: Context, additional_account_to_crank: typing.Optional[PublicKey]) -> typing.Sequence[PublicKey]:
|
||||
|
@ -77,7 +81,10 @@ class PerpMarket(Market):
|
|||
return [*bid_side.orders(), *ask_side.orders()]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"« 𝙿𝚎𝚛𝚙𝚜𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} [{self.address}] »"
|
||||
underlying: str = f"{self.underlying_perp_market}".replace("\n", "\n ")
|
||||
return f"""« 𝙿𝚎𝚛𝚙𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} [{self.address}]
|
||||
{underlying}
|
||||
»"""
|
||||
|
||||
|
||||
# # 🥭 PerpMarketStub class
|
||||
|
|
|
@ -44,8 +44,8 @@ class Token:
|
|||
return round(shifted, int(self.decimals))
|
||||
|
||||
def shift_to_native(self, value: Decimal) -> Decimal:
|
||||
divisor = Decimal(10 ** self.decimals)
|
||||
shifted = value * divisor
|
||||
multiplier = Decimal(10 ** self.decimals)
|
||||
shifted = value * multiplier
|
||||
return round(shifted, 0)
|
||||
|
||||
def symbol_matches(self, symbol: str) -> bool:
|
||||
|
|
Loading…
Reference in New Issue