All MarketOperations now properly use their equivalent MarketInstructionBuilder for building instructions.
This commit is contained in:
parent
6a15c81fa3
commit
80886c106c
|
@ -46,12 +46,12 @@ logging.warning(mango.WARNING_DISCLAIMER_TEXT)
|
|||
try:
|
||||
context = mango.Context.from_command_line_parameters(args)
|
||||
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
|
||||
margin_account_address = args.address
|
||||
account_address = args.address
|
||||
liquidator_name = args.name
|
||||
|
||||
logging.info(f"Context: {context}")
|
||||
logging.info(f"Wallet address: {wallet.address}")
|
||||
logging.info(f"Margin account address: {margin_account_address}")
|
||||
logging.info(f"Margin account address: {account_address}")
|
||||
|
||||
group = mango.Group.load(context)
|
||||
|
||||
|
@ -86,9 +86,9 @@ try:
|
|||
|
||||
# TODO - fetch prices when available for V3.
|
||||
# prices = group.fetch_token_prices(context)
|
||||
margin_account = mango.Account.load(context, margin_account_address, group)
|
||||
account = mango.Account.load(context, account_address, group)
|
||||
worthwhile_threshold = Decimal(0) # No threshold - don't take this into account.
|
||||
liquidatable_report = mango.LiquidatableReport.build(group, [], margin_account, worthwhile_threshold)
|
||||
liquidatable_report = mango.LiquidatableReport.build(group, [], account, worthwhile_threshold)
|
||||
transaction_id = account_liquidator.liquidate(liquidatable_report)
|
||||
if transaction_id is None:
|
||||
print("No transaction sent.")
|
||||
|
|
|
@ -58,17 +58,17 @@ for notify in args.notify_errors:
|
|||
logging.warning(mango.WARNING_DISCLAIMER_TEXT)
|
||||
|
||||
|
||||
def start_subscriptions(context: mango.Context, liquidation_processor: mango.LiquidationProcessor, fetch_prices: typing.Callable[[typing.Any], typing.Any], fetch_margin_accounts: typing.Callable[[typing.Any], typing.Any], throttle_reload_to_seconds: Decimal, throttle_ripe_update_to_seconds: Decimal):
|
||||
def start_subscriptions(context: mango.Context, liquidation_processor: mango.LiquidationProcessor, fetch_prices: typing.Callable[[typing.Any], typing.Any], fetch_accounts: typing.Callable[[typing.Any], typing.Any], throttle_reload_to_seconds: Decimal, throttle_ripe_update_to_seconds: Decimal):
|
||||
liquidation_processor.state = mango.LiquidationProcessorState.STARTING
|
||||
|
||||
logging.info("Starting margin account fetcher subscription")
|
||||
margin_account_subscription = rx.interval(float(throttle_reload_to_seconds)).pipe(
|
||||
account_subscription = rx.interval(float(throttle_reload_to_seconds)).pipe(
|
||||
ops.subscribe_on(context.pool_scheduler),
|
||||
ops.start_with(-1),
|
||||
ops.map(fetch_margin_accounts(context)),
|
||||
ops.map(fetch_accounts(context)),
|
||||
ops.catch(mango.observable_pipeline_error_reporter),
|
||||
ops.retry()
|
||||
).subscribe(mango.create_backpressure_skipping_observer(on_next=liquidation_processor.update_margin_accounts, on_error=mango.log_subscription_error))
|
||||
).subscribe(mango.create_backpressure_skipping_observer(on_next=liquidation_processor.update_accounts, on_error=mango.log_subscription_error))
|
||||
|
||||
logging.info("Starting price fetcher subscription")
|
||||
price_subscription = rx.interval(float(throttle_ripe_update_to_seconds)).pipe(
|
||||
|
@ -78,7 +78,7 @@ def start_subscriptions(context: mango.Context, liquidation_processor: mango.Liq
|
|||
ops.retry()
|
||||
).subscribe(mango.create_backpressure_skipping_observer(on_next=lambda piped: liquidation_processor.update_prices(piped[0], piped[1]), on_error=mango.log_subscription_error))
|
||||
|
||||
return margin_account_subscription, price_subscription
|
||||
return account_subscription, price_subscription
|
||||
|
||||
|
||||
try:
|
||||
|
@ -160,29 +160,29 @@ try:
|
|||
|
||||
return _fetch_prices
|
||||
|
||||
def fetch_margin_accounts(context):
|
||||
def fetch_accounts(context):
|
||||
def _actual_fetch():
|
||||
group = mango.Group.load(context)
|
||||
return mango.Account.load_ripe(context, group)
|
||||
|
||||
def _fetch_margin_accounts(_):
|
||||
def _fetch_accounts(_):
|
||||
with mango.retry_context("Margin Account Fetch",
|
||||
_actual_fetch,
|
||||
context.retry_pauses) as retrier:
|
||||
return retrier.run()
|
||||
return _fetch_margin_accounts
|
||||
return _fetch_accounts
|
||||
|
||||
class LiquidationProcessorSubscriptions:
|
||||
def __init__(self, margin_account: rx.core.typing.Disposable, price: rx.core.typing.Disposable):
|
||||
self.margin_account: rx.core.typing.Disposable = margin_account
|
||||
def __init__(self, account: rx.core.typing.Disposable, price: rx.core.typing.Disposable):
|
||||
self.account: rx.core.typing.Disposable = account
|
||||
self.price: rx.core.typing.Disposable = price
|
||||
|
||||
liquidation_processor = mango.LiquidationProcessor(
|
||||
context, liquidator_name, account_liquidator, wallet_balancer, worthwhile_threshold)
|
||||
margin_account_subscription, price_subscription = start_subscriptions(
|
||||
context, liquidation_processor, fetch_prices, fetch_margin_accounts, throttle_reload_to_seconds, throttle_ripe_update_to_seconds)
|
||||
account_subscription, price_subscription = start_subscriptions(
|
||||
context, liquidation_processor, fetch_prices, fetch_accounts, throttle_reload_to_seconds, throttle_ripe_update_to_seconds)
|
||||
|
||||
subscriptions = LiquidationProcessorSubscriptions(margin_account=margin_account_subscription,
|
||||
subscriptions = LiquidationProcessorSubscriptions(account=account_subscription,
|
||||
price=price_subscription)
|
||||
|
||||
def on_unhealthy(liquidation_processor: mango.LiquidationProcessor):
|
||||
|
@ -193,7 +193,7 @@ try:
|
|||
|
||||
logging.warning("Liquidation processor has been marked as unhealthy so recreating subscriptions.")
|
||||
try:
|
||||
subscriptions.margin_account.dispose()
|
||||
subscriptions.account.dispose()
|
||||
except Exception as exception:
|
||||
logging.warning(f"Ignoring problem disposing of margin account subscription: {exception}")
|
||||
try:
|
||||
|
@ -201,9 +201,9 @@ try:
|
|||
except Exception as exception:
|
||||
logging.warning(f"Ignoring problem disposing of margin account subscription: {exception}")
|
||||
|
||||
margin_account_subscription, price_subscription = start_subscriptions(
|
||||
context, liquidation_processor, fetch_prices, fetch_margin_accounts, throttle_reload_to_seconds, throttle_ripe_update_to_seconds)
|
||||
subscriptions.margin_account = margin_account_subscription
|
||||
account_subscription, price_subscription = start_subscriptions(
|
||||
context, liquidation_processor, fetch_prices, fetch_accounts, throttle_reload_to_seconds, throttle_ripe_update_to_seconds)
|
||||
subscriptions.account = account_subscription
|
||||
subscriptions.price = price_subscription
|
||||
|
||||
liquidation_processor.state_change.subscribe(on_next=on_unhealthy)
|
||||
|
|
|
@ -84,8 +84,7 @@ try:
|
|||
liquidation_processor = mango.LiquidationProcessor(context, liquidator_name, account_liquidator, wallet_balancer)
|
||||
|
||||
started_at = time.time()
|
||||
# ripe = group.load_ripe_margin_accounts()
|
||||
liquidation_processor.update_margin_accounts([])
|
||||
liquidation_processor.update_accounts([])
|
||||
|
||||
group = mango.Group.load(context) # Refresh group data
|
||||
# prices = group.fetch_token_prices(context)
|
||||
|
|
|
@ -30,7 +30,7 @@ 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"Could not find any margin accounts for '{wallet.address}'.")
|
||||
margin_account = accounts[0]
|
||||
account = accounts[0]
|
||||
|
||||
token = context.token_lookup.find_by_symbol(args.symbol)
|
||||
if token is None:
|
||||
|
@ -53,7 +53,7 @@ node_bank = root_bank.pick_node_bank(context)
|
|||
|
||||
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet)
|
||||
withdraw = mango.build_withdraw_instructions(
|
||||
context, wallet, group, margin_account, root_bank, node_bank, withdrawal_token_account, args.allow_borrow)
|
||||
context, wallet, group, account, root_bank, node_bank, withdrawal_token_account, args.allow_borrow)
|
||||
|
||||
all_instructions = signers + withdraw
|
||||
transaction_id = all_instructions.execute_and_unwrap_transaction_ids(context)[0]
|
||||
|
|
|
@ -155,12 +155,12 @@ class AccountScout:
|
|||
f"Account '{account_address}' has {len(token_accounts)} {basket_token.token.name} token account(s) with mint '{basket_token.token.mint}': {[ta.address for ta in token_accounts]}")
|
||||
|
||||
# May have one or more Mango Markets margin account, but it's optional for liquidating
|
||||
margin_accounts = Account.load_all_for_owner(context, account_address, group)
|
||||
if len(margin_accounts) == 0:
|
||||
accounts = Account.load_all_for_owner(context, account_address, group)
|
||||
if len(accounts) == 0:
|
||||
report.add_detail(f"Account '{account_address}' has no Mango Markets margin accounts.")
|
||||
else:
|
||||
for margin_account in margin_accounts:
|
||||
report.add_detail(f"Margin account: {margin_account}")
|
||||
for account in accounts:
|
||||
report.add_detail(f"Margin account: {account}")
|
||||
|
||||
return report
|
||||
|
||||
|
|
|
@ -22,11 +22,14 @@ from .group import Group
|
|||
from .market import Market
|
||||
from .marketoperations import MarketOperations, NullMarketOperations
|
||||
from .perpmarket import PerpMarket
|
||||
from .perpmarketinstructionbuilder import PerpMarketInstructionBuilder
|
||||
from .perpmarketoperations import PerpMarketOperations
|
||||
from .perpsmarket import PerpsMarket
|
||||
from .serummarket import SerumMarket
|
||||
from .serummarketinstructionbuilder import SerumMarketInstructionBuilder
|
||||
from .serummarketoperations import SerumMarketOperations
|
||||
from .spotmarket import SpotMarket
|
||||
from .spotmarketinstructionbuilder import SpotMarketInstructionBuilder
|
||||
from .spotmarketoperations import SpotMarketOperations
|
||||
from .wallet import Wallet
|
||||
|
||||
|
@ -39,18 +42,27 @@ def create_market_operations(context: Context, wallet: Wallet, dry_run: bool, ma
|
|||
if dry_run:
|
||||
return NullMarketOperations(market.symbol, reporter)
|
||||
elif isinstance(market, SerumMarket):
|
||||
return SerumMarketOperations(context, wallet, market, reporter)
|
||||
serum_market_instruction_builder: SerumMarketInstructionBuilder = SerumMarketInstructionBuilder.load(
|
||||
context, wallet, market)
|
||||
return SerumMarketOperations(context, wallet, market, serum_market_instruction_builder, reporter)
|
||||
elif isinstance(market, SpotMarket):
|
||||
group = Group.load(context, market.group_address)
|
||||
margin_accounts = Account.load_all_for_owner(context, wallet.address, group)
|
||||
return SpotMarketOperations(context, wallet, group, margin_accounts[0], market, reporter)
|
||||
accounts = Account.load_all_for_owner(context, wallet.address, group)
|
||||
account = accounts[0]
|
||||
spot_market_instruction_builder: SpotMarketInstructionBuilder = SpotMarketInstructionBuilder.load(
|
||||
context, wallet, group, account, market)
|
||||
return SpotMarketOperations(context, wallet, group, account, market, spot_market_instruction_builder, reporter)
|
||||
elif isinstance(market, PerpsMarket):
|
||||
group = Group.load(context, context.group_id)
|
||||
margin_accounts = Account.load_all_for_owner(context, wallet.address, group)
|
||||
accounts = Account.load_all_for_owner(context, wallet.address, group)
|
||||
account = accounts[0]
|
||||
perp_market_info = group.perp_markets[0]
|
||||
if perp_market_info is None:
|
||||
raise Exception("Perp market not found at index 0.")
|
||||
perp_market = PerpMarket.load(context, perp_market_info.address, group)
|
||||
return PerpMarketOperations(market.symbol, context, wallet, margin_accounts[0], perp_market, reporter)
|
||||
perp_market_instruction_builder: PerpMarketInstructionBuilder = PerpMarketInstructionBuilder.load(
|
||||
context, wallet, group, account, perp_market)
|
||||
|
||||
return PerpMarketOperations(market.symbol, context, wallet, perp_market_instruction_builder, account, perp_market, reporter)
|
||||
else:
|
||||
raise Exception(f"Could not find order placer for market {market.symbol}")
|
||||
|
|
|
@ -119,6 +119,20 @@ class Group(AddressableAccount):
|
|||
raise Exception(f"Group account not found at address '{group_address}'")
|
||||
return Group.parse(context, account_info)
|
||||
|
||||
def find_spot_market_index(self, spot_market_address: PublicKey) -> int:
|
||||
for index, spot in enumerate(self.spot_markets):
|
||||
if spot is not None and spot.address == spot_market_address:
|
||||
return index
|
||||
|
||||
raise Exception(f"Could not find spot market {spot_market_address} in group {self.address}")
|
||||
|
||||
def find_perp_market_index(self, perp_market_address: PublicKey) -> int:
|
||||
for index, pm in enumerate(self.perp_markets):
|
||||
if pm is not None and pm.address == perp_market_address:
|
||||
return index
|
||||
|
||||
raise Exception(f"Could not find perp market {perp_market_address} in group {self.address}")
|
||||
|
||||
def fetch_balances(self, context: Context, root_address: PublicKey) -> typing.Sequence[TokenValue]:
|
||||
balances: typing.List[TokenValue] = []
|
||||
sol_balance = context.fetch_sol_balance(root_address)
|
||||
|
|
|
@ -71,12 +71,7 @@ from .wallet import Wallet
|
|||
|
||||
def _ensure_openorders(context: Context, wallet: Wallet, group: Group, account: Account, market: Market) -> typing.Tuple[PublicKey, CombinableInstructions]:
|
||||
spot_market_address = market.state.public_key()
|
||||
market_index: int = -1
|
||||
for index, spot in enumerate(group.spot_markets):
|
||||
if spot is not None and spot.address == spot_market_address:
|
||||
market_index = index
|
||||
if market_index == -1:
|
||||
raise Exception(f"Could not find spot market {spot_market_address} in group {group.address}")
|
||||
market_index = group.find_spot_market_index(spot_market_address)
|
||||
|
||||
open_orders_address = account.spot_open_orders[market_index]
|
||||
if open_orders_address is not None:
|
||||
|
@ -274,7 +269,7 @@ def build_compound_serum_place_order_instructions(context: Context, wallet: Wall
|
|||
#
|
||||
|
||||
|
||||
def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margin_account: Account, perp_market: PerpMarket, order: Order) -> CombinableInstructions:
|
||||
def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, account: Account, perp_market: PerpMarket, order: Order) -> CombinableInstructions:
|
||||
# Prefer cancelling by client ID so we don't have to keep track of the order side.
|
||||
if order.client_id != 0:
|
||||
data: bytes = layouts.CANCEL_PERP_ORDER_BY_CLIENT_ID.build(
|
||||
|
@ -302,8 +297,8 @@ def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margi
|
|||
instructions = [
|
||||
TransactionInstruction(
|
||||
keys=[
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=margin_account.group.address),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=margin_account.address),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=account.group.address),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=account.address),
|
||||
AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.address),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.bids),
|
||||
|
@ -317,7 +312,7 @@ def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margi
|
|||
return CombinableInstructions(signers=[], instructions=instructions)
|
||||
|
||||
|
||||
def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: Group, margin_account: Account, perp_market: PerpMarket, price: Decimal, quantity: Decimal, client_order_id: int, side: Side, order_type: OrderType) -> CombinableInstructions:
|
||||
def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, perp_market: PerpMarket, price: Decimal, quantity: Decimal, client_order_id: int, side: Side, order_type: OrderType) -> CombinableInstructions:
|
||||
# { buy: 0, sell: 1 }
|
||||
raw_side: int = 1 if side == Side.SELL else 0
|
||||
# { limit: 0, ioc: 1, postOnly: 2 }
|
||||
|
@ -346,7 +341,7 @@ def build_place_perp_order_instructions(context: Context, wallet: Wallet, group:
|
|||
TransactionInstruction(
|
||||
keys=[
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=group.address),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=margin_account.address),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=account.address),
|
||||
AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=group.cache),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.address),
|
||||
|
@ -354,7 +349,7 @@ def build_place_perp_order_instructions(context: Context, wallet: Wallet, group:
|
|||
AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.asks),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.event_queue),
|
||||
*list([AccountMeta(is_signer=False, is_writable=False,
|
||||
pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in margin_account.spot_open_orders])
|
||||
pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in account.spot_open_orders])
|
||||
],
|
||||
program_id=context.program_id,
|
||||
data=layouts.PLACE_PERP_ORDER.build(
|
||||
|
@ -437,12 +432,12 @@ def build_create_account_instructions(context: Context, wallet: Wallet, group: G
|
|||
# quantity: u64,
|
||||
# allow_borrow: bool,
|
||||
# },
|
||||
def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group, margin_account: Account, root_bank: RootBank, node_bank: NodeBank, token_account: TokenAccount, allow_borrow: bool) -> CombinableInstructions:
|
||||
def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group, account: Account, root_bank: RootBank, node_bank: NodeBank, token_account: TokenAccount, allow_borrow: bool) -> CombinableInstructions:
|
||||
value = token_account.value.shift_to_native().value
|
||||
withdraw = TransactionInstruction(
|
||||
keys=[
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=group.address),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=margin_account.address),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=account.address),
|
||||
AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=group.cache),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=root_bank.address),
|
||||
|
@ -452,7 +447,7 @@ def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group,
|
|||
AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID),
|
||||
*list([AccountMeta(is_signer=False, is_writable=False,
|
||||
pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in margin_account.spot_open_orders])
|
||||
pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in account.spot_open_orders])
|
||||
],
|
||||
program_id=context.program_id,
|
||||
data=layouts.WITHDRAW.build({
|
||||
|
|
|
@ -25,14 +25,14 @@ from .tokenvalue import TokenValue
|
|||
|
||||
|
||||
class LiquidationEvent:
|
||||
def __init__(self, timestamp: datetime.datetime, liquidator_name: str, group_name: str, succeeded: bool, signature: str, wallet_address: PublicKey, margin_account_address: PublicKey, balances_before: typing.Sequence[TokenValue], balances_after: typing.Sequence[TokenValue]):
|
||||
def __init__(self, timestamp: datetime.datetime, liquidator_name: str, group_name: str, succeeded: bool, signature: str, wallet_address: PublicKey, account_address: PublicKey, balances_before: typing.Sequence[TokenValue], balances_after: typing.Sequence[TokenValue]):
|
||||
self.timestamp: datetime.datetime = timestamp
|
||||
self.liquidator_name: str = liquidator_name
|
||||
self.group_name: str = group_name
|
||||
self.succeeded: bool = succeeded
|
||||
self.signature: str = signature
|
||||
self.wallet_address: PublicKey = wallet_address
|
||||
self.margin_account_address: PublicKey = margin_account_address
|
||||
self.account_address: PublicKey = account_address
|
||||
self.balances_before: typing.Sequence[TokenValue] = balances_before
|
||||
self.balances_after: typing.Sequence[TokenValue] = balances_after
|
||||
self.changes: typing.Sequence[TokenValue] = TokenValue.changes(balances_before, balances_after)
|
||||
|
@ -45,7 +45,7 @@ class LiquidationEvent:
|
|||
🗃️ Group: {self.group_name}
|
||||
📇 Signature: {self.signature}
|
||||
👛 Wallet: {self.wallet_address}
|
||||
💳 Margin Account: {self.margin_account_address}
|
||||
💳 Margin Account: {self.account_address}
|
||||
💸 Changes:
|
||||
{changes_text}
|
||||
»"""
|
||||
|
|
|
@ -78,11 +78,11 @@ class LiquidationProcessor:
|
|||
self.state: LiquidationProcessorState = LiquidationProcessorState.STARTING
|
||||
self.state_change: EventSource[LiquidationProcessor] = EventSource[LiquidationProcessor]()
|
||||
|
||||
def update_margin_accounts(self, ripe_margin_accounts: typing.Sequence[Account]):
|
||||
def update_accounts(self, ripe_accounts: typing.Sequence[Account]):
|
||||
self.logger.info(
|
||||
f"Received {len(ripe_margin_accounts)} ripe 🥭 margin accounts to process - prices last updated {self.prices_updated_at:%Y-%m-%d %H:%M:%S}")
|
||||
f"Received {len(ripe_accounts)} ripe 🥭 margin accounts to process - prices last updated {self.prices_updated_at:%Y-%m-%d %H:%M:%S}")
|
||||
self._check_update_recency("prices", self.prices_updated_at)
|
||||
self.ripe_accounts = ripe_margin_accounts
|
||||
self.ripe_accounts = ripe_accounts
|
||||
self.ripe_accounts_updated_at = datetime.now()
|
||||
# If this is the first time through, mark ourselves as Healthy.
|
||||
if self.state == LiquidationProcessorState.STARTING:
|
||||
|
@ -105,8 +105,8 @@ class LiquidationProcessor:
|
|||
|
||||
report: typing.List[str] = []
|
||||
updated: typing.List[LiquidatableReport] = []
|
||||
for margin_account in self.ripe_accounts:
|
||||
updated += [LiquidatableReport.build(group, prices, margin_account, self.worthwhile_threshold)]
|
||||
for account in self.ripe_accounts:
|
||||
updated += [LiquidatableReport.build(group, prices, account, self.worthwhile_threshold)]
|
||||
|
||||
liquidatable = list(filter(lambda report: report.state & LiquidatableState.LIQUIDATABLE, updated))
|
||||
report += [f"Of those {len(updated)} ripe accounts, {len(liquidatable)} are liquidatable."]
|
||||
|
@ -139,15 +139,15 @@ class LiquidationProcessor:
|
|||
self.account_liquidator.liquidate(highest)
|
||||
self.wallet_balancer.balance(prices)
|
||||
|
||||
updated_margin_account = Account.load(self.context, highest.account.address, group)
|
||||
updated_account = Account.load(self.context, highest.account.address, group)
|
||||
updated_report = LiquidatableReport.build(
|
||||
group, prices, updated_margin_account, highest.worthwhile_threshold)
|
||||
group, prices, updated_account, highest.worthwhile_threshold)
|
||||
if not (updated_report.state & LiquidatableState.WORTHWHILE):
|
||||
self.logger.info(
|
||||
f"Margin account {updated_margin_account.address} has been drained and is no longer worthwhile.")
|
||||
f"Margin account {updated_account.address} has been drained and is no longer worthwhile.")
|
||||
else:
|
||||
self.logger.info(
|
||||
f"Margin account {updated_margin_account.address} is still worthwhile - putting it back on list.")
|
||||
f"Margin account {updated_account.address} is still worthwhile - putting it back on list.")
|
||||
to_process += [updated_report]
|
||||
except Exception as exception:
|
||||
self.logger.error(
|
||||
|
|
|
@ -242,7 +242,7 @@ class CsvFileNotificationTarget(NotificationTarget):
|
|||
with open(self.filename, "a") as csvfile:
|
||||
result = "Succeeded" if event.succeeded else "Failed"
|
||||
row_data = [event.timestamp, event.liquidator_name, event.group_name, result,
|
||||
event.signature, event.wallet_address, event.margin_account_address]
|
||||
event.signature, event.wallet_address, event.account_address]
|
||||
for change in event.changes:
|
||||
row_data += [f"{change.value:.8f}", change.token.name]
|
||||
file_writer = csv.writer(csvfile, quoting=csv.QUOTE_MINIMAL)
|
||||
|
|
|
@ -59,17 +59,11 @@ class PerpMarket(AddressableAccount):
|
|||
self.scaler: PublicKey = scaler
|
||||
self.total_liquidity_points: Decimal = total_liquidity_points
|
||||
|
||||
market_index = -1
|
||||
for index, pm in enumerate(group.perp_markets):
|
||||
if pm is not None and pm.address == self.address:
|
||||
market_index = index
|
||||
if market_index == -1:
|
||||
raise Exception(f"Could not find perp market {self.address} in group {group.address}")
|
||||
self.market_index = market_index
|
||||
self.market_index = group.find_perp_market_index(self.address)
|
||||
|
||||
base_token = group.tokens[market_index]
|
||||
base_token = group.tokens[self.market_index]
|
||||
if base_token is None:
|
||||
raise Exception(f"Could not find base token at index {market_index} for perp market {self.address}.")
|
||||
raise Exception(f"Could not find base token at index {self.market_index} for perp market {self.address}.")
|
||||
self.base_token: TokenInfo = base_token
|
||||
|
||||
quote_token = group.tokens[-1]
|
||||
|
|
|
@ -24,10 +24,10 @@ from .accountinfo import AccountInfo
|
|||
from .combinableinstructions import CombinableInstructions
|
||||
from .context import Context
|
||||
from .marketoperations import MarketOperations
|
||||
from .instructions import build_cancel_perp_order_instructions, build_place_perp_order_instructions
|
||||
from .orderbookside import OrderBookSide
|
||||
from .orders import Order, OrderType, Side
|
||||
from .perpmarket import PerpMarket
|
||||
from .perpmarketinstructionbuilder import PerpMarketInstructionBuilder
|
||||
from .wallet import Wallet
|
||||
|
||||
|
||||
|
@ -39,41 +39,35 @@ from .wallet import Wallet
|
|||
|
||||
class PerpMarketOperations(MarketOperations):
|
||||
def __init__(self, market_name: str, context: Context, wallet: Wallet,
|
||||
margin_account: Account, perp_market: PerpMarket,
|
||||
market_instruction_builder: PerpMarketInstructionBuilder,
|
||||
account: Account, perp_market: PerpMarket,
|
||||
reporter: typing.Callable[[str], None] = None):
|
||||
super().__init__()
|
||||
self.market_name: str = market_name
|
||||
self.context: Context = context
|
||||
self.wallet: Wallet = wallet
|
||||
self.margin_account: Account = margin_account
|
||||
self.market_instruction_builder: PerpMarketInstructionBuilder = market_instruction_builder
|
||||
self.account: Account = account
|
||||
self.perp_market: PerpMarket = perp_market
|
||||
self.reporter = reporter or (lambda _: None)
|
||||
|
||||
def cancel_order(self, order: Order) -> typing.Sequence[str]:
|
||||
report = f"Cancelling order on market {self.market_name}."
|
||||
self.logger.info(report)
|
||||
self.reporter(report)
|
||||
|
||||
self.reporter(f"Cancelling order {order.id} on market {self.market_name}.")
|
||||
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
|
||||
cancel_instructions = build_cancel_perp_order_instructions(
|
||||
self.context, self.wallet, self.margin_account, self.perp_market, order)
|
||||
all_instructions = signers + cancel_instructions
|
||||
|
||||
return all_instructions.execute_and_unwrap_transaction_ids(self.context)
|
||||
cancel = self.market_instruction_builder.build_cancel_order_instructions(order)
|
||||
return (signers + cancel).execute_and_unwrap_transaction_ids(self.context)
|
||||
|
||||
def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order:
|
||||
client_order_id = self.context.random_client_id()
|
||||
report = f"Placing {order_type} {side} order for size {size} at price {price} on market {self.market_name} using client ID {client_order_id}."
|
||||
client_id: int = self.context.random_client_id()
|
||||
report: str = f"Placing {order_type} {side} order for size {size} at price {price} on market {self.market_name} with ID {client_id}."
|
||||
self.logger.info(report)
|
||||
self.reporter(report)
|
||||
|
||||
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
|
||||
place_instructions = build_place_perp_order_instructions(
|
||||
self.context, self.wallet, self.perp_market.group, self.margin_account, self.perp_market, price, size, client_order_id, side, order_type)
|
||||
all_instructions = signers + place_instructions
|
||||
all_instructions.execute(self.context)
|
||||
|
||||
return Order(id=0, side=side, price=price, size=size, client_id=client_order_id, owner=self.margin_account.address)
|
||||
place = self.market_instruction_builder.build_place_order_instructions(
|
||||
side, order_type, price, size, client_id)
|
||||
(signers + place).execute(self.context)
|
||||
return Order(id=0, side=side, price=price, size=size, client_id=client_id, owner=self.account.address)
|
||||
|
||||
def load_orders(self) -> typing.Sequence[Order]:
|
||||
bids_address: PublicKey = self.perp_market.bids
|
||||
|
@ -88,7 +82,7 @@ class PerpMarketOperations(MarketOperations):
|
|||
orders = self.load_orders()
|
||||
mine = []
|
||||
for order in orders:
|
||||
if order.owner == self.margin_account.address:
|
||||
if order.owner == self.account.address:
|
||||
mine += [order]
|
||||
|
||||
return mine
|
||||
|
|
|
@ -14,19 +14,17 @@
|
|||
# [Email](mailto:hello@blockworks.foundation)
|
||||
|
||||
|
||||
import pyserum.enums
|
||||
import typing
|
||||
|
||||
from decimal import Decimal
|
||||
from pyserum.market import Market
|
||||
from solana.rpc.types import TxOpts
|
||||
|
||||
from .combinableinstructions import CombinableInstructions
|
||||
from .context import Context
|
||||
from .marketoperations import MarketOperations
|
||||
from .openorders import OpenOrders
|
||||
from .orders import Order, OrderType, Side
|
||||
from .serummarket import SerumMarket
|
||||
from .tokenaccount import TokenAccount
|
||||
from .serummarketinstructionbuilder import SerumMarketInstructionBuilder
|
||||
from .wallet import Wallet
|
||||
|
||||
|
||||
|
@ -36,17 +34,13 @@ from .wallet import Wallet
|
|||
#
|
||||
|
||||
class SerumMarketOperations(MarketOperations):
|
||||
def __init__(self, context: Context, wallet: Wallet, serum_market: SerumMarket, reporter: typing.Callable[[str], None] = None):
|
||||
def __init__(self, context: Context, wallet: Wallet, serum_market: SerumMarket, market_instruction_builder: SerumMarketInstructionBuilder, reporter: typing.Callable[[str], None] = None):
|
||||
super().__init__()
|
||||
self.context: Context = context
|
||||
self.wallet: Wallet = wallet
|
||||
self.serum_market: SerumMarket = serum_market
|
||||
self.market: Market = Market.load(context.client, serum_market.address, context.dex_program_id)
|
||||
all_open_orders = OpenOrders.load_for_market_and_owner(
|
||||
context, serum_market.address, wallet.address, context.dex_program_id, serum_market.base.decimals, serum_market.quote.decimals)
|
||||
if len(all_open_orders) == 0:
|
||||
raise Exception(f"No OpenOrders account available for market {serum_market}.")
|
||||
self.open_orders = all_open_orders[0]
|
||||
self.market_instruction_builder: SerumMarketInstructionBuilder = market_instruction_builder
|
||||
|
||||
def report(text):
|
||||
self.logger.info(text)
|
||||
|
@ -61,34 +55,22 @@ class SerumMarketOperations(MarketOperations):
|
|||
self.reporter = just_log
|
||||
|
||||
def cancel_order(self, order: Order) -> typing.Sequence[str]:
|
||||
self.reporter(
|
||||
f"Cancelling order {order.id} in openorders {self.open_orders.address} on market {self.serum_market.symbol}.")
|
||||
try:
|
||||
response = self.market.cancel_order_by_client_id(
|
||||
self.wallet.account, self.open_orders.address, order.id,
|
||||
TxOpts(preflight_commitment=self.context.commitment))
|
||||
return [self.context.unwrap_transaction_id_or_raise_exception(response)]
|
||||
except Exception as exception:
|
||||
self.logger.warning(f"Failed to cancel order {order.id} - continuing. {exception}")
|
||||
return [""]
|
||||
self.reporter(f"Cancelling order {order.id} on market {self.serum_market.symbol}.")
|
||||
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
|
||||
cancel = self.market_instruction_builder.build_cancel_order_instructions(order)
|
||||
return (signers + cancel).execute_and_unwrap_transaction_ids(self.context)
|
||||
|
||||
def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order:
|
||||
client_id: int = self.context.random_client_id()
|
||||
report: str = f"Placing {order_type} {side} order for size {size} at price {price} on market {self.serum_market.symbol} with ID {client_id}."
|
||||
self.logger.info(report)
|
||||
self.reporter(report)
|
||||
serum_order_type = pyserum.enums.OrderType.POST_ONLY if order_type == OrderType.POST_ONLY else pyserum.enums.OrderType.IOC if order_type == OrderType.IOC else pyserum.enums.OrderType.LIMIT
|
||||
serum_side = pyserum.enums.Side.BUY if side == Side.BUY else pyserum.enums.Side.SELL
|
||||
payer_token = self.serum_market.quote if side == Side.BUY else self.serum_market.base
|
||||
token_account = TokenAccount.fetch_largest_for_owner_and_token(self.context, self.wallet.address, payer_token)
|
||||
if token_account is None:
|
||||
raise Exception(f"Could not find payer token account for token {payer_token.symbol}.")
|
||||
|
||||
response = self.market.place_order(token_account.address, self.wallet.account,
|
||||
serum_order_type, serum_side, float(price), float(size),
|
||||
client_id, TxOpts(preflight_commitment=self.context.commitment))
|
||||
self.context.unwrap_or_raise_exception(response)
|
||||
return Order(id=0, side=side, price=price, size=size, client_id=client_id, owner=self.open_orders.address)
|
||||
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
|
||||
place = self.market_instruction_builder.build_place_order_instructions(
|
||||
side, order_type, price, size, client_id)
|
||||
(signers + place).execute(self.context)
|
||||
return Order(id=0, side=side, price=price, size=size, client_id=client_id, owner=self.market_instruction_builder.open_orders.address)
|
||||
|
||||
def load_orders(self) -> typing.Sequence[Order]:
|
||||
asks = self.market.load_asks()
|
||||
|
|
|
@ -75,12 +75,7 @@ class SpotMarketInstructionBuilder(MarketInstructionBuilder):
|
|||
if quote_token_account is None:
|
||||
raise Exception(f"Could not find source token account for quote token {spot_market.quote.symbol}.")
|
||||
|
||||
market_index: int = -1
|
||||
for index, spot in enumerate(group.spot_markets):
|
||||
if spot is not None and spot.address == spot_market.address:
|
||||
market_index = index
|
||||
if market_index == -1:
|
||||
raise Exception(f"Could not find spot market {spot_market.address} in group {group.address}")
|
||||
market_index = group.find_spot_market_index(spot_market.address)
|
||||
|
||||
return SpotMarketInstructionBuilder(context, wallet, group, account, spot_market, raw_market, base_token_account, quote_token_account, market_index, fee_discount_token_address)
|
||||
|
||||
|
|
|
@ -18,21 +18,18 @@ import itertools
|
|||
import typing
|
||||
|
||||
from decimal import Decimal
|
||||
from pyserum.market import Market
|
||||
from pyserum.market.orderbook import OrderBook as SerumOrderBook
|
||||
from pyserum.market.types import Order as SerumOrder
|
||||
from solana.publickey import PublicKey
|
||||
|
||||
from .account import Account
|
||||
from .accountinfo import AccountInfo
|
||||
from .combinableinstructions import CombinableInstructions
|
||||
from .context import Context
|
||||
from .group import Group
|
||||
from .instructions import build_compound_spot_place_order_instructions, build_cancel_spot_order_instructions
|
||||
from .marketoperations import MarketOperations
|
||||
from .orders import Order, OrderType, Side
|
||||
from .spotmarket import SpotMarket
|
||||
from .tokenaccount import TokenAccount
|
||||
from .spotmarketinstructionbuilder import SpotMarketInstructionBuilder
|
||||
from .wallet import Wallet
|
||||
|
||||
|
||||
|
@ -42,25 +39,17 @@ from .wallet import Wallet
|
|||
#
|
||||
|
||||
class SpotMarketOperations(MarketOperations):
|
||||
def __init__(self, context: Context, wallet: Wallet, group: Group, account: Account, spot_market: SpotMarket, reporter: typing.Callable[[str], None] = None):
|
||||
def __init__(self, context: Context, wallet: Wallet, group: Group, account: Account, spot_market: SpotMarket, market_instruction_builder: SpotMarketInstructionBuilder, reporter: typing.Callable[[str], None] = None):
|
||||
super().__init__()
|
||||
self.context: Context = context
|
||||
self.wallet: Wallet = wallet
|
||||
self.group: Group = group
|
||||
self.account: Account = account
|
||||
self.spot_market: SpotMarket = spot_market
|
||||
self.market: Market = Market.load(context.client, spot_market.address, context.dex_program_id)
|
||||
self._serum_fee_discount_token_address: typing.Optional[PublicKey] = None
|
||||
self._serum_fee_discount_token_address_loaded: bool = False
|
||||
self.market_instruction_builder: SpotMarketInstructionBuilder = market_instruction_builder
|
||||
|
||||
market_index: int = -1
|
||||
for index, spot in enumerate(self.group.spot_markets):
|
||||
if spot is not None and spot.address == self.spot_market.address:
|
||||
market_index = index
|
||||
if market_index == -1:
|
||||
raise Exception(f"Could not find spot market {self.spot_market.address} in group {self.group.address}")
|
||||
|
||||
self.group_market_index: int = market_index
|
||||
self.market_index = group.find_spot_market_index(spot_market.address)
|
||||
self.open_orders = self.account.spot_open_orders[self.market_index]
|
||||
|
||||
def report(text):
|
||||
self.logger.info(text)
|
||||
|
@ -74,66 +63,31 @@ class SpotMarketOperations(MarketOperations):
|
|||
else:
|
||||
self.reporter = just_log
|
||||
|
||||
@property
|
||||
def serum_fee_discount_token_address(self) -> typing.Optional[PublicKey]:
|
||||
if self._serum_fee_discount_token_address_loaded:
|
||||
return self._serum_fee_discount_token_address
|
||||
|
||||
# SRM is always the token Serum uses for fee discounts
|
||||
token = self.context.token_lookup.find_by_symbol("SRM")
|
||||
if token is None:
|
||||
self._serum_fee_discount_token_address_loaded = True
|
||||
self._serum_fee_discount_token_address = None
|
||||
return self._serum_fee_discount_token_address
|
||||
|
||||
fee_discount_token_account = TokenAccount.fetch_largest_for_owner_and_token(
|
||||
self.context, self.wallet.address, token)
|
||||
if fee_discount_token_account is not None:
|
||||
self._serum_fee_discount_token_address = fee_discount_token_account.address
|
||||
|
||||
self._serum_fee_discount_token_address_loaded = True
|
||||
return self._serum_fee_discount_token_address
|
||||
|
||||
def cancel_order(self, order: Order) -> typing.Sequence[str]:
|
||||
report = f"Cancelling order {order.id} on market {self.spot_market.symbol}."
|
||||
self.logger.info(report)
|
||||
self.reporter(report)
|
||||
|
||||
open_orders = self.account.spot_open_orders[self.group_market_index]
|
||||
|
||||
self.reporter(f"Cancelling order {order.id} on market {self.spot_market.symbol}.")
|
||||
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
|
||||
cancel_instructions = build_cancel_spot_order_instructions(
|
||||
self.context, self.wallet, self.group, self.account, self.market, order, open_orders)
|
||||
all_instructions = signers + cancel_instructions
|
||||
return all_instructions.execute_and_unwrap_transaction_ids(self.context)
|
||||
cancel = self.market_instruction_builder.build_cancel_order_instructions(order)
|
||||
return (signers + cancel).execute_and_unwrap_transaction_ids(self.context)
|
||||
|
||||
def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order:
|
||||
payer_token = self.spot_market.quote if side == Side.BUY else self.spot_market.base
|
||||
payer_token_account = TokenAccount.fetch_largest_for_owner_and_token(
|
||||
self.context, self.wallet.address, payer_token)
|
||||
if payer_token_account is None:
|
||||
raise Exception(f"Could not find a source token account for token {payer_token}.")
|
||||
|
||||
client_order_id = self.context.random_client_id()
|
||||
report = f"Placing {order_type} {side} order for size {size} at price {price} on market {self.spot_market.symbol} using client ID {client_order_id}."
|
||||
client_id: int = self.context.random_client_id()
|
||||
report: str = f"Placing {order_type} {side} order for size {size} at price {price} on market {self.spot_market.symbol} with ID {client_id}."
|
||||
self.logger.info(report)
|
||||
self.reporter(report)
|
||||
|
||||
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
|
||||
place_instructions = build_compound_spot_place_order_instructions(
|
||||
self.context, self.wallet, self.group, self.account, self.market, payer_token_account.address,
|
||||
order_type, side, price, size, client_order_id, self.serum_fee_discount_token_address)
|
||||
place = self.market_instruction_builder.build_place_order_instructions(
|
||||
side, order_type, price, size, client_id)
|
||||
(signers + place).execute(self.context)
|
||||
|
||||
all_instructions = signers + place_instructions
|
||||
all_instructions.execute(self.context)
|
||||
|
||||
return Order(id=0, side=side, price=price, size=size, client_id=client_order_id, owner=self.account.address)
|
||||
return Order(id=0, side=side, price=price, size=size, client_id=client_id, owner=self.open_orders)
|
||||
|
||||
def _load_serum_orders(self) -> typing.Sequence[SerumOrder]:
|
||||
raw_market = self.market_instruction_builder.raw_market
|
||||
[bids_info, asks_info] = AccountInfo.load_multiple(
|
||||
self.context, [self.market.state.bids(), self.market.state.asks()])
|
||||
bids_orderbook = SerumOrderBook.from_bytes(self.market.state, bids_info.data)
|
||||
asks_orderbook = SerumOrderBook.from_bytes(self.market.state, asks_info.data)
|
||||
self.context, [raw_market.state.bids(), raw_market.state.asks()])
|
||||
bids_orderbook = SerumOrderBook.from_bytes(raw_market.state, bids_info.data)
|
||||
asks_orderbook = SerumOrderBook.from_bytes(raw_market.state, asks_info.data)
|
||||
|
||||
return list(itertools.chain(bids_orderbook.orders(), asks_orderbook.orders()))
|
||||
|
||||
|
@ -146,12 +100,11 @@ class SpotMarketOperations(MarketOperations):
|
|||
return orders
|
||||
|
||||
def load_my_orders(self) -> typing.Sequence[Order]:
|
||||
open_orders_account = self.account.spot_open_orders[self.group_market_index]
|
||||
if not open_orders_account:
|
||||
if not self.open_orders:
|
||||
return []
|
||||
|
||||
all_orders = self._load_serum_orders()
|
||||
serum_orders = [o for o in all_orders if o.open_order_address == open_orders_account]
|
||||
serum_orders = [o for o in all_orders if o.open_order_address == self.open_orders]
|
||||
orders: typing.List[Order] = []
|
||||
for serum_order in serum_orders:
|
||||
orders += [Order.from_serum_order(serum_order)]
|
||||
|
|
Loading…
Reference in New Issue