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