All MarketOperations now properly use their equivalent MarketInstructionBuilder for building instructions.

This commit is contained in:
Geoff Taylor 2021-07-12 18:26:35 +01:00
parent 6a15c81fa3
commit 80886c106c
16 changed files with 134 additions and 196 deletions

View File

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

View File

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

View File

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

View File

@ -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]

View File

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

View File

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

View File

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

View File

@ -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({

View File

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

View File

@ -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(

View File

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

View File

@ -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]

View File

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

View File

@ -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()

View File

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

View File

@ -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)]