New approach to transactions allowing them to be split up.

* CombinableTransactions now separated out.
* CombinableTransactions are now size-aware when being executed, and will automatically split into batches for execution if they are too big.
* New MarketInstructionBuilder approach - each market type can now have its own way of building instructions for common operations.
This commit is contained in:
Geoff Taylor 2021-07-12 17:18:56 +01:00
parent faf514cde1
commit 6a15c81fa3
22 changed files with 649 additions and 221 deletions

View File

@ -7,7 +7,6 @@ import sys
import typing
from solana.publickey import PublicKey
from solana.transaction import Transaction
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
@ -32,16 +31,15 @@ token_account: typing.Optional[mango.TokenAccount] = mango.TokenAccount.load(con
if (token_account is None) or (token_account.value.token.mint != wrapped_sol.mint):
raise Exception(f"Account {args.address} is not a {wrapped_sol.name} account.")
transaction = Transaction()
payer = wallet.address
signers = mango.InstructionData.from_wallet(wallet)
payer: PublicKey = wallet.address
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet)
close_instruction = mango.build_close_spl_account_instructions(context, wallet, args.address)
print(f"Closing account: {args.address} with balance {token_account.value.value} lamports.")
all_instructions = signers + close_instruction
transaction_id = all_instructions.execute_and_unwrap_transaction_id(context)
transaction_id = all_instructions.execute_and_unwrap_transaction_ids(context)[0]
print(f"Waiting on transaction ID: {transaction_id}")
context.wait_for_confirmation(transaction_id)

View File

@ -22,8 +22,8 @@ wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_id)
signers: mango.InstructionData = mango.InstructionData.from_wallet(wallet)
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet)
init = mango.build_create_account_instructions(context, wallet, group)
all_instructions = signers + init
transaction_id = all_instructions.execute_and_unwrap_transaction_id(context)
transaction_id = all_instructions.execute_and_unwrap_transaction_ids(context)[0]
print("Signature:", transaction_id)

View File

@ -47,7 +47,7 @@ try:
else:
trade_executor = mango.SerumImmediateTradeExecutor(context, wallet, adjustment_factor)
transaction_id = trade_executor.buy(symbol, args.quantity)
transaction_id = trade_executor.buy(symbol, args.quantity)[0]
if args.wait:
logging.info(f"Waiting on {transaction_id}")
context.wait_for_confirmation(transaction_id)

View File

@ -47,7 +47,7 @@ try:
else:
trade_executor = mango.SerumImmediateTradeExecutor(context, wallet, adjustment_factor)
transaction_id = trade_executor.sell(symbol, args.quantity)
transaction_id = trade_executor.sell(symbol, args.quantity)[0]
if args.wait:
logging.info(f"Waiting on {transaction_id}")
context.wait_for_confirmation(transaction_id)

View File

@ -39,7 +39,7 @@ try:
if market is None:
raise Exception(f"Could not find market {market_symbol}")
instructions = mango.InstructionData.from_wallet(wallet)
instructions: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet)
group = mango.Group.load(context)
accounts = mango.Account.load_all_for_owner(context, wallet.address, group)
@ -77,7 +77,7 @@ try:
if args.dry_run:
print("Skipping transaction processing - dry run is set.")
else:
transaction_id = instructions.execute_and_unwrap_transaction_id(context)
transaction_id = instructions.execute_and_unwrap_transaction_ids(context)[0]
if args.wait:
print(f"Waiting on {transaction_id}")
context.wait_for_confirmation(transaction_id)

View File

@ -7,7 +7,6 @@ import sys
from decimal import Decimal
from solana.account import Account
from solana.transaction import Transaction
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
@ -32,9 +31,8 @@ largest_token_account = mango.TokenAccount.fetch_largest_for_owner_and_token(
if largest_token_account is None:
raise Exception(f"No {wrapped_sol.name} accounts found for owner {wallet.address}.")
transaction = Transaction()
wrapped_sol_account = Account()
signers = mango.InstructionData.from_signers([wallet.account, wrapped_sol_account])
wrapped_sol_account: Account = Account()
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_signers([wallet.account, wrapped_sol_account])
create_instructions = mango.build_create_spl_account_instructions(
context, wallet, wrapped_sol, wrapped_sol_account.public_key())
unwrap_instructions = mango.build_transfer_spl_tokens_instructions(
@ -48,7 +46,7 @@ print(f" Temporary account: {wrapped_sol_account.public_key()}")
print(f" Source: {largest_token_account.address}")
print(f" Destination: {wallet.address}")
transaction_id = all_instructions.execute_and_unwrap_transaction_id(context)
transaction_id = all_instructions.execute_and_unwrap_transaction_ids(context)[0]
print(f"Waiting on transaction ID: {transaction_id}")
context.wait_for_confirmation(transaction_id)

View File

@ -51,10 +51,10 @@ if root_bank is None:
node_bank = root_bank.pick_node_bank(context)
signers: mango.InstructionData = mango.InstructionData.from_wallet(wallet)
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)
all_instructions = signers + withdraw
transaction_id = all_instructions.execute_and_unwrap_transaction_id(context)
transaction_id = all_instructions.execute_and_unwrap_transaction_ids(context)[0]
print("Signature:", transaction_id)

View File

@ -7,7 +7,6 @@ import sys
from decimal import Decimal
from solana.account import Account
from solana.transaction import Transaction
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
@ -29,9 +28,8 @@ wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
wrapped_sol = context.token_lookup.find_by_symbol_or_raise("SOL")
amount_to_transfer = int(args.quantity * mango.SOL_DECIMAL_DIVISOR)
transaction = Transaction()
wrapped_sol_account = Account()
signers = mango.InstructionData.from_signers([wallet.account, wrapped_sol_account])
wrapped_sol_account: Account = Account()
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_signers([wallet.account, wrapped_sol_account])
create_instructions = mango.build_create_spl_account_instructions(
context, wallet, wrapped_sol, wrapped_sol_account.public_key(), amount_to_transfer)
@ -52,7 +50,7 @@ else:
close_instruction = mango.build_close_spl_account_instructions(context, wallet, wrapped_sol_account.public_key())
all_instructions = all_instructions + wrap_instruction + close_instruction
transaction_id = all_instructions.execute_and_unwrap_transaction_id(context)
transaction_id = all_instructions.execute_and_unwrap_transaction_ids(context)[0]
print(f"Waiting on transaction ID: {transaction_id}")
context.wait_for_confirmation(transaction_id)

View File

@ -5,6 +5,7 @@ from .accountliquidator import AccountLiquidator, NullAccountLiquidator
from .accountscout import ScoutReport, AccountScout
from .addressableaccount import AddressableAccount
from .balancesheet import BalanceSheet
from .combinableinstructions import CombinableInstructions
from .constants import SYSTEM_PROGRAM_ADDRESS, SOL_MINT_ADDRESS, SOL_DECIMALS, SOL_DECIMAL_DIVISOR, WARNING_DISCLAIMER_TEXT, MangoConstants
from .context import Context, default_cluster, default_cluster_url, default_program_id, default_dex_program_id, default_group_name, default_group_id
from .createmarketoperations import create_market_operations
@ -12,12 +13,13 @@ from .encoding import decode_binary, encode_binary, encode_key, encode_int
from .group import Group
from .idsjsontokenlookup import IdsJsonTokenLookup
from .idsjsonmarketlookup import IdsJsonMarketLookup
from .instructions import InstructionData, build_create_solana_account_instructions, build_create_spl_account_instructions, build_transfer_spl_tokens_instructions, build_close_spl_account_instructions, build_create_serum_open_orders_instructions, build_serum_place_order_instructions, build_serum_consume_events_instructions, build_serum_settle_instructions, build_spot_place_order_instructions, build_cancel_spot_order_instructions, build_cancel_perp_order_instructions, build_mango_consume_events_instructions, build_create_account_instructions, build_place_perp_order_instructions, build_withdraw_instructions
from .instructions import build_create_solana_account_instructions, build_create_spl_account_instructions, build_transfer_spl_tokens_instructions, build_close_spl_account_instructions, build_create_serum_open_orders_instructions, build_serum_place_order_instructions, build_serum_consume_events_instructions, build_serum_settle_instructions, build_spot_place_order_instructions, build_cancel_spot_order_instructions, build_cancel_perp_order_instructions, build_mango_consume_events_instructions, build_create_account_instructions, build_place_perp_order_instructions, build_withdraw_instructions
from .instructiontype import InstructionType
from .liquidatablereport import LiquidatableState, LiquidatableReport
from .liquidationevent import LiquidationEvent
from .liquidationprocessor import LiquidationProcessor, LiquidationProcessorState
from .market import Market
from .marketinstructionbuilder import MarketInstructionBuilder, NullMarketInstructionBuilder
from .marketlookup import MarketLookup, CompoundMarketLookup
from .marketoperations import MarketOperations, NullMarketOperations
from .metadata import Metadata
@ -31,6 +33,7 @@ from .oracle import OracleSource, Price, Oracle, OracleProvider
from .oraclefactory import create_oracle_provider
from .perpmarket import PerpMarket
from .perpmarketinfo import PerpMarketInfo
from .perpmarketinstructionbuilder import PerpMarketInstructionBuilder
from .perpmarketoperations import PerpMarketOperations
from .perpsmarket import PerpsMarket
from .reconnectingwebsocket import ReconnectingWebsocket
@ -38,10 +41,12 @@ from .retrier import RetryWithPauses, retry_context
from .rootbank import NodeBank, RootBank
from .serummarket import SerumMarket
from .serummarketlookup import SerumMarketLookup
from .serummarketinstructionbuilder import SerumMarketInstructionBuilder
from .serummarketoperations import SerumMarketOperations
from .spltokenlookup import SplTokenLookup
from .spotmarket import SpotMarket
from .spotmarketinfo import SpotMarketInfo
from .spotmarketinstructionbuilder import SpotMarketInstructionBuilder
from .token import Token, SolToken
from .tokenaccount import TokenAccount
from .tokeninfo import TokenInfo

View File

@ -0,0 +1,130 @@
# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
# [Docs](https://docs.mango.markets/)
# [Discord](https://discord.gg/67jySBhxrg)
# [Twitter](https://twitter.com/mangomarkets)
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import logging
import typing
from solana.account import Account as SolanaAccount
from solana.blockhash import Blockhash
from solana.publickey import PublicKey
from solana.transaction import Transaction, TransactionInstruction
from .context import Context
from .wallet import Wallet
_MAXIMUM_TRANSACTION_LENGTH = 1280 - 40 - 8
_SIGNATURE_LENGTH = 64
# 🥭 CombinableInstructions class
#
# This class wraps up zero or more Solana instructions and signers, and allows instances to be combined
# easily into a single instance. This instance can then be executed.
#
# This allows simple uses like, for example:
# ```
# (signers + place_orders + settle + crank).execute(context)
# ```
#
class CombinableInstructions():
def __init__(self, signers: typing.Sequence[SolanaAccount], instructions: typing.Sequence[TransactionInstruction]):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.signers: typing.Sequence[SolanaAccount] = signers
self.instructions: typing.Sequence[TransactionInstruction] = instructions
@staticmethod
def empty() -> "CombinableInstructions":
return CombinableInstructions(signers=[], instructions=[])
@staticmethod
def from_signers(signers: typing.Sequence[SolanaAccount]) -> "CombinableInstructions":
return CombinableInstructions(signers=signers, instructions=[])
@staticmethod
def from_wallet(wallet: Wallet) -> "CombinableInstructions":
return CombinableInstructions(signers=[wallet.account], instructions=[])
@staticmethod
def from_instruction(instruction: TransactionInstruction) -> "CombinableInstructions":
return CombinableInstructions(signers=[], instructions=[instruction])
# This is a quick-and-dirty way to find out the size the transaction will be. There's an upper limit
# on transaction size of 1232 so we need to keep all transactions below this size.
@staticmethod
def transaction_size(signers: typing.Sequence[SolanaAccount], instructions: typing.Sequence[TransactionInstruction]) -> int:
inspector = Transaction()
inspector.recent_blockhash = Blockhash(str(PublicKey(3)))
inspector.instructions.extend(instructions)
inspector.sign(*signers)
signed_data = inspector.serialize_message()
length: int = len(signed_data)
# Signature count length
length += 1
# Signatures
length += (len(inspector.signatures) * _SIGNATURE_LENGTH)
return length
def __add__(self, new_instruction_data: "CombinableInstructions") -> "CombinableInstructions":
all_signers = [*self.signers, *new_instruction_data.signers]
all_instructions = [*self.instructions, *new_instruction_data.instructions]
return CombinableInstructions(signers=all_signers, instructions=all_instructions)
def execute(self, context: Context) -> typing.Any:
vetted_chunks: typing.List[typing.List[TransactionInstruction]] = []
current_chunk: typing.List[TransactionInstruction] = []
for instruction in self.instructions:
# current_chunk += [instruction]
in_progress_chunk = current_chunk + [instruction]
if CombinableInstructions.transaction_size(self.signers, in_progress_chunk) < _MAXIMUM_TRANSACTION_LENGTH:
current_chunk = in_progress_chunk
else:
vetted_chunks += [current_chunk]
current_chunk = [instruction]
all_chunks = vetted_chunks + [current_chunk]
self.logger.info(f"Running instructions in {len(all_chunks)} transaction(s).")
results = []
for chunk in all_chunks:
transaction = Transaction()
transaction.instructions.extend(chunk)
response = context.client.send_transaction(transaction, *self.signers, opts=context.transaction_options)
results += [context.unwrap_or_raise_exception(response)]
return results
def execute_and_unwrap_transaction_ids(self, context: Context) -> typing.Sequence[str]:
return typing.cast(typing.Sequence[str], self.execute(context))
def __str__(self) -> str:
report: typing.List[str] = []
for index, signer in enumerate(self.signers):
report += [f"Signer[{index}]: {signer}"]
for index, instruction in enumerate(self.instructions):
for index, key in enumerate(instruction.keys):
report += [f"Key[{index}]: {key.pubkey} {key.is_signer: <5} {key.is_writable: <5}"]
report += [f"Program ID: {instruction.program_id}"]
report += ["Data: " + "".join("{:02x}".format(x) for x in instruction.data)]
return "\n".join(report)
def __repr__(self) -> str:
return f"{self}"

View File

@ -26,11 +26,12 @@ from solana.account import Account as SolanaAccount
from solana.publickey import PublicKey
from solana.system_program import CreateAccountParams, create_account
from solana.sysvar import SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY
from solana.transaction import AccountMeta, Transaction, TransactionInstruction
from solana.transaction import AccountMeta, TransactionInstruction
from spl.token.constants import ACCOUNT_LEN, TOKEN_PROGRAM_ID
from spl.token.instructions import CloseAccountParams, InitializeAccountParams, Transfer2Params, close_account, initialize_account, transfer2
from .account import Account
from .combinableinstructions import CombinableInstructions
from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context
from .group import Group
@ -56,71 +57,19 @@ from .wallet import Wallet
# the function signature in future.
#
class InstructionData():
def __init__(self, signers: typing.Sequence[SolanaAccount], instructions: typing.Sequence[TransactionInstruction]):
self.signers: typing.Sequence[SolanaAccount] = signers
self.instructions: typing.Sequence[TransactionInstruction] = instructions
@staticmethod
def empty() -> "InstructionData":
return InstructionData(signers=[], instructions=[])
@staticmethod
def from_signers(signers: typing.Sequence[SolanaAccount]) -> "InstructionData":
return InstructionData(signers=signers, instructions=[])
@staticmethod
def from_wallet(wallet: Wallet) -> "InstructionData":
return InstructionData(signers=[wallet.account], instructions=[])
@staticmethod
def from_instruction(instruction: TransactionInstruction) -> "InstructionData":
return InstructionData(signers=[], instructions=[instruction])
def __add__(self, new_instruction_data: "InstructionData") -> "InstructionData":
all_signers = [*self.signers, *new_instruction_data.signers]
all_instructions = [*self.instructions, *new_instruction_data.instructions]
return InstructionData(signers=all_signers, instructions=all_instructions)
def execute(self, context: Context) -> typing.Any:
transaction = Transaction()
transaction.instructions.extend(self.instructions)
response = context.client.send_transaction(transaction, *self.signers, opts=context.transaction_options)
return context.unwrap_or_raise_exception(response)
def execute_and_unwrap_transaction_id(self, context: Context) -> typing.Any:
return typing.cast(str, self.execute(context))
def __str__(self) -> str:
report: typing.List[str] = []
for index, signer in enumerate(self.signers):
report += [f"Signer[{index}]: {signer}"]
for index, instruction in enumerate(self.instructions):
for index, key in enumerate(instruction.keys):
report += [f"Key[{index}]: {key.pubkey} {key.is_signer: <5} {key.is_writable: <5}"]
report += [f"Program ID: {instruction.program_id}"]
report += ["Data: " + "".join("{:02x}".format(x) for x in instruction.data)]
return "\n".join(report)
def __repr__(self) -> str:
return f"{self}"
# # 🥭 _ensure_openorders function
#
# Unlike most functions in this file, `_ensure_openorders()` returns a tuple, not just an `InstructionData`.
# Unlike most functions in this file, `_ensure_openorders()` returns a tuple, not just an `CombinableInstructions`.
#
# The idea is: callers just want to know the OpenOrders address, but if it doesn't exist they may need to add
# the instructions and signers for its creation before they try to use it.
#
# This function will always return the proper OpenOrders address, and will also return the `InstructionData` to
# This function will always return the proper OpenOrders address, and will also return the `CombinableInstructions` to
# create it (which will be empty of signers and instructions if the OpenOrders already exists).
#
def _ensure_openorders(context: Context, wallet: Wallet, group: Group, account: Account, market: Market) -> typing.Tuple[PublicKey, InstructionData]:
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):
@ -131,7 +80,7 @@ def _ensure_openorders(context: Context, wallet: Wallet, group: Group, account:
open_orders_address = account.spot_open_orders[market_index]
if open_orders_address is not None:
return open_orders_address, InstructionData.empty()
return open_orders_address, CombinableInstructions.empty()
creation = build_create_solana_account_instructions(
context, wallet, context.dex_program_id, layouts.OPEN_ORDERS.sizeof())
@ -150,14 +99,14 @@ def _ensure_openorders(context: Context, wallet: Wallet, group: Group, account:
# necesary.
#
def build_create_solana_account_instructions(context: Context, wallet: Wallet, program_id: PublicKey, size: int, lamports: int = 0) -> InstructionData:
def build_create_solana_account_instructions(context: Context, wallet: Wallet, program_id: PublicKey, size: int, lamports: int = 0) -> CombinableInstructions:
minimum_balance_response = context.client.get_minimum_balance_for_rent_exemption(
size, commitment=context.commitment)
minimum_balance = context.unwrap_or_raise_exception(minimum_balance_response)
account = SolanaAccount()
create_instruction = create_account(
CreateAccountParams(wallet.address, account.public_key(), lamports + minimum_balance, size, program_id))
return InstructionData(signers=[account], instructions=[create_instruction])
return CombinableInstructions(signers=[account], instructions=[create_instruction])
# # 🥭 build_create_spl_account_instructions function
@ -166,12 +115,12 @@ def build_create_solana_account_instructions(context: Context, wallet: Wallet, p
# necesary.
#
def build_create_spl_account_instructions(context: Context, wallet: Wallet, token: Token, address: PublicKey, lamports: int = 0) -> InstructionData:
def build_create_spl_account_instructions(context: Context, wallet: Wallet, token: Token, address: PublicKey, lamports: int = 0) -> CombinableInstructions:
create_instructions = build_create_solana_account_instructions(context, wallet, TOKEN_PROGRAM_ID, ACCOUNT_LEN,
lamports)
initialize_instruction = initialize_account(InitializeAccountParams(
TOKEN_PROGRAM_ID, address, token.mint, wallet.address))
return create_instructions + InstructionData(signers=[], instructions=[initialize_instruction])
return create_instructions + CombinableInstructions(signers=[], instructions=[initialize_instruction])
# # 🥭 build_transfer_spl_tokens_instructions function
@ -179,11 +128,11 @@ def build_create_spl_account_instructions(context: Context, wallet: Wallet, toke
# Creates an instruction to transfer SPL tokens from one account to another.
#
def build_transfer_spl_tokens_instructions(context: Context, wallet: Wallet, token: Token, source: PublicKey, destination: PublicKey, quantity: Decimal) -> InstructionData:
def build_transfer_spl_tokens_instructions(context: Context, wallet: Wallet, token: Token, source: PublicKey, destination: PublicKey, quantity: Decimal) -> CombinableInstructions:
amount = int(quantity * (10 ** token.decimals))
instructions = [transfer2(Transfer2Params(TOKEN_PROGRAM_ID, source, token.mint,
destination, wallet.address, amount, int(token.decimals)))]
return InstructionData(signers=[], instructions=instructions)
return CombinableInstructions(signers=[], instructions=instructions)
# # 🥭 build_close_spl_account_instructions function
@ -191,8 +140,8 @@ def build_transfer_spl_tokens_instructions(context: Context, wallet: Wallet, tok
# Creates an instructio to close an SPL token account and transfers any remaining lamports to the wallet.
#
def build_close_spl_account_instructions(context: Context, wallet: Wallet, address: PublicKey) -> InstructionData:
return InstructionData(signers=[], instructions=[close_account(CloseAccountParams(TOKEN_PROGRAM_ID, address, wallet.address, wallet.address))])
def build_close_spl_account_instructions(context: Context, wallet: Wallet, address: PublicKey) -> CombinableInstructions:
return CombinableInstructions(signers=[], instructions=[close_account(CloseAccountParams(TOKEN_PROGRAM_ID, address, wallet.address, wallet.address))])
# # 🥭 build_create_serum_open_orders_instructions function
@ -200,7 +149,7 @@ def build_close_spl_account_instructions(context: Context, wallet: Wallet, addre
# Creates a Serum openorders-creating instruction.
#
def build_create_serum_open_orders_instructions(context: Context, wallet: Wallet, market: Market) -> InstructionData:
def build_create_serum_open_orders_instructions(context: Context, wallet: Wallet, market: Market) -> CombinableInstructions:
new_open_orders_account = SolanaAccount()
response = context.client.get_minimum_balance_for_rent_exemption(
layouts.OPEN_ORDERS.sizeof(), commitment=context.commitment)
@ -212,7 +161,7 @@ def build_create_serum_open_orders_instructions(context: Context, wallet: Wallet
program_id=market.state.program_id(),
)
return InstructionData(signers=[new_open_orders_account], instructions=[instruction])
return CombinableInstructions(signers=[new_open_orders_account], instructions=[instruction])
# # 🥭 build_serum_place_order_instructions function
@ -220,7 +169,7 @@ def build_create_serum_open_orders_instructions(context: Context, wallet: Wallet
# Creates a Serum order-placing instruction using V3 of the NewOrder instruction.
#
def build_serum_place_order_instructions(context: Context, wallet: Wallet, market: Market, source: PublicKey, open_orders_address: PublicKey, order_type: OrderType, side: Side, price: Decimal, quantity: Decimal, client_id: int, fee_discount_address: typing.Optional[PublicKey]) -> InstructionData:
def build_serum_place_order_instructions(context: Context, wallet: Wallet, market: Market, source: PublicKey, open_orders_address: PublicKey, order_type: OrderType, side: Side, price: Decimal, quantity: Decimal, client_id: int, fee_discount_address: typing.Optional[PublicKey]) -> CombinableInstructions:
serum_order_type: SerumOrderType = SerumOrderType.POST_ONLY if order_type == OrderType.POST_ONLY else SerumOrderType.IOC if order_type == OrderType.IOC else SerumOrderType.LIMIT
serum_side: SerumSide = SerumSide.SELL if side == Side.SELL else SerumSide.BUY
@ -236,7 +185,7 @@ def build_serum_place_order_instructions(context: Context, wallet: Wallet, marke
fee_discount_address
)
return InstructionData(signers=[], instructions=[instruction])
return CombinableInstructions(signers=[], instructions=[instruction])
# # 🥭 build_serum_consume_events_instructions function
@ -244,7 +193,7 @@ def build_serum_place_order_instructions(context: Context, wallet: Wallet, marke
# Creates an event-consuming 'crank' instruction.
#
def build_serum_consume_events_instructions(context: Context, wallet: Wallet, market: Market, open_orders_addresses: typing.Sequence[PublicKey], limit: int = 32) -> InstructionData:
def build_serum_consume_events_instructions(context: Context, wallet: Wallet, market: Market, open_orders_addresses: typing.Sequence[PublicKey], limit: int = 32) -> CombinableInstructions:
instruction = consume_events(ConsumeEventsParams(
market=market.state.public_key(),
event_queue=market.state.event_queue(),
@ -258,7 +207,7 @@ def build_serum_consume_events_instructions(context: Context, wallet: Wallet, ma
random_account = SolanaAccount().public_key()
instruction.keys.append(AccountMeta(random_account, is_signer=False, is_writable=False))
instruction.keys.append(AccountMeta(random_account, is_signer=False, is_writable=False))
return InstructionData(signers=[], instructions=[instruction])
return CombinableInstructions(signers=[], instructions=[instruction])
# # 🥭 build_serum_settle_instructions function
@ -266,7 +215,7 @@ def build_serum_consume_events_instructions(context: Context, wallet: Wallet, ma
# Creates a 'settle' instruction.
#
def build_serum_settle_instructions(context: Context, wallet: Wallet, market: Market, open_orders_address: PublicKey, base_token_account_address: PublicKey, quote_token_account_address: PublicKey) -> InstructionData:
def build_serum_settle_instructions(context: Context, wallet: Wallet, market: Market, open_orders_address: PublicKey, base_token_account_address: PublicKey, quote_token_account_address: PublicKey) -> CombinableInstructions:
vault_signer = PublicKey.create_program_address(
[bytes(market.state.public_key()), market.state.vault_signer_nonce().to_bytes(8, byteorder="little")],
market.state.program_id(),
@ -285,7 +234,7 @@ def build_serum_settle_instructions(context: Context, wallet: Wallet, market: Ma
)
)
return InstructionData(signers=[], instructions=[instruction])
return CombinableInstructions(signers=[], instructions=[instruction])
# # 🥭 build_compound_serum_place_order_instructions function
@ -308,7 +257,7 @@ def build_serum_settle_instructions(context: Context, wallet: Wallet, market: Ma
# orderbook).
#
def build_compound_serum_place_order_instructions(context: Context, wallet: Wallet, market: Market, source: PublicKey, open_orders_address: PublicKey, all_open_orders_addresses: typing.Sequence[PublicKey], order_type: OrderType, side: Side, price: Decimal, quantity: Decimal, client_id: int, base_token_account_address: PublicKey, quote_token_account_address: PublicKey, fee_discount_address: typing.Optional[PublicKey], consume_limit: int = 32) -> InstructionData:
def build_compound_serum_place_order_instructions(context: Context, wallet: Wallet, market: Market, source: PublicKey, open_orders_address: PublicKey, all_open_orders_addresses: typing.Sequence[PublicKey], order_type: OrderType, side: Side, price: Decimal, quantity: Decimal, client_id: int, base_token_account_address: PublicKey, quote_token_account_address: PublicKey, fee_discount_address: typing.Optional[PublicKey], consume_limit: int = 32) -> CombinableInstructions:
place_order = build_serum_place_order_instructions(
context, wallet, market, source, open_orders_address, order_type, side, price, quantity, client_id, fee_discount_address)
consume_events = build_serum_consume_events_instructions(
@ -325,7 +274,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) -> InstructionData:
def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margin_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(
@ -365,10 +314,10 @@ def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margi
data=data
)
]
return InstructionData(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) -> InstructionData:
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:
# { buy: 0, sell: 1 }
raw_side: int = 1 if side == Side.SELL else 0
# { limit: 0, ioc: 1, postOnly: 2 }
@ -418,10 +367,10 @@ def build_place_perp_order_instructions(context: Context, wallet: Wallet, group:
})
)
]
return InstructionData(signers=[], instructions=instructions)
return CombinableInstructions(signers=[], instructions=instructions)
def build_mango_consume_events_instructions(context: Context, wallet: Wallet, group: Group, account: Account, perp_market: PerpMarket, limit: Decimal = Decimal(32)) -> InstructionData:
def build_mango_consume_events_instructions(context: Context, wallet: Wallet, group: Group, account: Account, perp_market: PerpMarket, limit: Decimal = Decimal(32)) -> CombinableInstructions:
# Accounts expected by this instruction (6):
# 0. `[]` mangoGroupPk
# 1. `[]` perpMarketPk
@ -443,10 +392,10 @@ def build_mango_consume_events_instructions(context: Context, wallet: Wallet, gr
})
)
]
return InstructionData(signers=[], instructions=instructions)
return CombinableInstructions(signers=[], instructions=instructions)
def build_create_account_instructions(context: Context, wallet: Wallet, group: Group) -> InstructionData:
def build_create_account_instructions(context: Context, wallet: Wallet, group: Group) -> CombinableInstructions:
create_account_instructions = build_create_solana_account_instructions(
context, wallet, context.program_id, layouts.MANGO_ACCOUNT)
mango_account_address = create_account_instructions.signers[0].public_key()
@ -465,7 +414,7 @@ def build_create_account_instructions(context: Context, wallet: Wallet, group: G
program_id=context.program_id,
data=layouts.INIT_MANGO_ACCOUNT.build({})
)
return create_account_instructions + InstructionData(signers=[], instructions=[init])
return create_account_instructions + CombinableInstructions(signers=[], instructions=[init])
# /// Withdraw funds that were deposited earlier.
@ -488,7 +437,7 @@ 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) -> InstructionData:
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:
value = token_account.value.shift_to_native().value
withdraw = TransactionInstruction(
keys=[
@ -512,7 +461,7 @@ def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group,
})
)
return InstructionData(signers=[], instructions=[withdraw])
return CombinableInstructions(signers=[], instructions=[withdraw])
# # 🥭 build_mango_place_order_instructions function
@ -524,8 +473,8 @@ def build_spot_place_order_instructions(context: Context, wallet: Wallet, group:
market: Market,
order_type: OrderType, side: Side, price: Decimal,
quantity: Decimal, client_id: int,
fee_discount_address: typing.Optional[PublicKey]) -> InstructionData:
instructions: InstructionData = InstructionData.empty()
fee_discount_address: typing.Optional[PublicKey]) -> CombinableInstructions:
instructions: CombinableInstructions = CombinableInstructions.empty()
open_orders_address, create_open_orders = _ensure_openorders(context, wallet, group, account, market)
instructions += create_open_orders
@ -627,13 +576,13 @@ def build_spot_place_order_instructions(context: Context, wallet: Wallet, group:
)
)
return instructions + InstructionData(signers=[], instructions=[place_spot_instruction])
return instructions + CombinableInstructions(signers=[], instructions=[place_spot_instruction])
def build_compound_spot_place_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account,
market: Market, source: PublicKey, order_type: OrderType,
side: Side, price: Decimal, quantity: Decimal, client_id: int,
fee_discount_address: typing.Optional[PublicKey]) -> InstructionData:
fee_discount_address: typing.Optional[PublicKey]) -> CombinableInstructions:
_, create_open_orders = _ensure_openorders(context, wallet, group, account, market)
place_order = build_spot_place_order_instructions(context, wallet, group, account, market, order_type,
@ -654,7 +603,7 @@ def build_compound_spot_place_order_instructions(context: Context, wallet: Walle
# quote_token_account = TokenAccount.fetch_largest_for_owner_and_token(
# context, wallet.address, quote_token_info.token)
# settle: InstructionData = InstructionData.empty()
# settle: CombinableInstructions = CombinableInstructions.empty()
# if base_token_account is not None and quote_token_account is not None:
# open_order_accounts = market.find_open_orders_accounts_for_owner(wallet.address)
# settlement_open_orders = [oo for oo in open_order_accounts if oo.market == market.state.public_key()]
@ -672,7 +621,7 @@ def build_compound_spot_place_order_instructions(context: Context, wallet: Walle
#
def build_cancel_spot_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, market: Market, order: Order, open_orders_address: PublicKey) -> InstructionData:
def build_cancel_spot_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, market: Market, order: Order, open_orders_address: PublicKey) -> CombinableInstructions:
# { buy: 0, sell: 1 }
raw_side: int = 1 if order.side == Side.SELL else 0
@ -710,4 +659,4 @@ def build_cancel_spot_order_instructions(context: Context, wallet: Wallet, group
})
)
]
return InstructionData(signers=[], instructions=instructions)
return CombinableInstructions(signers=[], instructions=instructions)

View File

@ -0,0 +1,94 @@
# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
# [Docs](https://docs.mango.markets/)
# [Discord](https://discord.gg/67jySBhxrg)
# [Twitter](https://twitter.com/mangomarkets)
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import abc
import logging
from decimal import Decimal
from .combinableinstructions import CombinableInstructions
from .orders import Order, OrderType, Side
# # 🥭 MarketInstructionBuilder class
#
# This abstracts the process of buiding instructions for placing orders and cancelling orders.
#
# It's abstracted because we may want to have different implementations for different market types.
#
# Whichever choice is made, the calling code shouldn't have to care. It should be able to
# use its `MarketInstructionBuilder` class as simply as:
# ```
# instruction_builder.build_cancel_order_instructions(order)
# ```
#
# As a matter of policy for all InstructionBuidlers, construction and build_* methods should all work with
# existing data, requiring no fetches from Solana or other sources. All necessary data should all be loaded
# on initial setup in the `load()` method.
#
class MarketInstructionBuilder(metaclass=abc.ABCMeta):
def __init__(self):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@abc.abstractmethod
def build_cancel_order_instructions(self, order: Order) -> CombinableInstructions:
raise NotImplementedError(
"MarketInstructionBuilder.build_cancel_order_instructions() is not implemented on the base type.")
@abc.abstractmethod
def build_place_order_instructions(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal, client_order_id: int) -> CombinableInstructions:
raise NotImplementedError(
"MarketInstructionBuilder.build_place_order_instructions() is not implemented on the base type.")
@abc.abstractmethod
def build_settle_instructions(self) -> CombinableInstructions:
raise NotImplementedError(
"MarketInstructionBuilder.build_settle_instructions() is not implemented on the base type.")
@abc.abstractmethod
def build_crank_instructions(self, limit: Decimal = Decimal(32)) -> CombinableInstructions:
raise NotImplementedError(
"MarketInstructionBuilder.build_crank_instructions() is not implemented on the base type.")
def __repr__(self) -> str:
return f"{self}"
# # 🥭 NullMarketInstructionBuilder class
#
# A null, no-op, dry-run trade executor that can be plugged in anywhere a `MarketInstructionBuilder`
# is expected, but which will not actually trade.
#
class NullMarketInstructionBuilder(MarketInstructionBuilder):
def __init__(self):
super().__init__()
def build_cancel_order_instructions(self, order: Order) -> CombinableInstructions:
return CombinableInstructions.empty()
def build_place_order_instructions(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal, client_order_id: int) -> CombinableInstructions:
return CombinableInstructions.empty()
def build_settle_instructions(self) -> CombinableInstructions:
return CombinableInstructions.empty()
def build_crank_instructions(self, limit: Decimal = Decimal(32)) -> CombinableInstructions:
return CombinableInstructions.empty()
def __str__(self) -> str:
return """« 𝙽𝚞𝚕𝚕𝙼𝚊𝚛𝚔𝚎𝚝𝙸𝚗𝚜𝚝𝚛𝚞𝚌𝚝𝚒𝚘𝚗𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »"""

View File

@ -15,6 +15,7 @@
import logging
import mango
import traceback
import typing
@ -23,30 +24,32 @@ from decimal import Decimal
from mango.marketmaking.modelstate import ModelState
# # 🥭 SimpleMarketMaker class
# # 🥭 MarketMaker class
#
# An event-driven market-maker.
#
class MarketMaker:
def __init__(self, wallet: mango.Wallet, market: mango.Market,
market_instruction_builder: mango.MarketInstructionBuilder,
spread_ratio: Decimal, position_size_ratio: Decimal):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.wallet: mango.Wallet = wallet
self.market: mango.Market = market
self.market_instruction_builder: mango.MarketInstructionBuilder = market_instruction_builder
self.spread_ratio: Decimal = spread_ratio
self.position_size_ratio: Decimal = position_size_ratio
self.buys = []
self.sells = []
self.buy_client_ids: typing.List[int] = []
self.sell_client_ids: typing.List[int] = []
def calculate_order_prices(self, model_state: ModelState):
def calculate_order_prices(self, model_state: ModelState) -> typing.Tuple[Decimal, Decimal]:
price: mango.Price = model_state.price
bid = price.mid_price - (price.mid_price * self.spread_ratio)
ask = price.mid_price + (price.mid_price * self.spread_ratio)
bid: Decimal = price.mid_price - (price.mid_price * self.spread_ratio)
ask: Decimal = price.mid_price + (price.mid_price * self.spread_ratio)
return (bid, ask)
def calculate_order_sizes(self, model_state: ModelState):
def calculate_order_sizes(self, model_state: ModelState) -> typing.Tuple[Decimal, Decimal]:
price: mango.Price = model_state.price
inventory: typing.Sequence[typing.Optional[mango.TokenValue]] = model_state.account.net_assets
base_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.base)
@ -60,92 +63,42 @@ class MarketMaker:
total = (base_tokens.value * price.mid_price) + quote_tokens.value
position_size = total * self.position_size_ratio
buy_size = position_size / price.mid_price
sell_size = position_size / price.mid_price
buy_size: Decimal = position_size / price.mid_price
sell_size: Decimal = position_size / price.mid_price
return (buy_size, sell_size)
def pulse_perp(self, context: mango.Context, model_state: ModelState):
def pulse(self, context: mango.Context, model_state: ModelState):
try:
bid, ask = self.calculate_order_prices(model_state)
buy_size, sell_size = self.calculate_order_sizes(model_state)
payer = mango.InstructionData.from_wallet(self.wallet)
perp_market = model_state.perp_market
perp_account = model_state.account.perp_accounts[perp_market.market_index]
payer = mango.CombinableInstructions.from_wallet(self.wallet)
cancellations = mango.InstructionData.empty()
print("Client IDs", [client_id for client_id in perp_account.open_orders.client_order_ids if client_id != 0])
for client_order_id in perp_account.open_orders.client_order_ids:
if client_order_id != 0:
self.logger.info(f"Cancelling order with client ID: {client_order_id}")
order = mango.Order(id=0, client_id=client_order_id, owner=self.wallet.address,
side=mango.Side.BUY, price=Decimal(0), size=Decimal(0))
cancel = mango.build_cancel_perp_order_instructions(
context, self.wallet, model_state.account, perp_market, order)
cancellations = mango.CombinableInstructions.empty()
for order_id, client_id in model_state.placed_order_ids:
if client_id != 0:
self.logger.info(f"Cancelling order with client ID: {client_id}")
side = mango.Side.BUY if client_id in self.buy_client_ids else mango.Side.SELL
order = mango.Order(id=int(order_id), client_id=int(client_id), owner=self.wallet.address,
side=side, price=Decimal(0), size=Decimal(0))
cancel = self.market_instruction_builder.build_cancel_order_instructions(order)
cancellations += cancel
buy_client_id = context.random_client_id()
buy = mango.build_place_perp_order_instructions(
context, self.wallet, model_state.group, model_state.account, perp_market, bid, buy_size, buy_client_id, mango.Side.BUY, mango.OrderType.LIMIT)
self.buy_client_ids += [buy_client_id]
self.logger.info(f"Placing BUY order for {buy_size} at price {bid} with client ID: {buy_client_id}")
buy = self.market_instruction_builder.build_place_order_instructions(
mango.Side.BUY, mango.OrderType.POST_ONLY, bid, buy_size, buy_client_id)
sell_client_id = context.random_client_id()
sell = mango.build_place_perp_order_instructions(
context, self.wallet, model_state.group, model_state.account, perp_market, ask, sell_size, sell_client_id, mango.Side.SELL, mango.OrderType.LIMIT)
self.sell_client_ids += [sell_client_id]
self.logger.info(f"Placing SELL order for {sell_size} at price {ask} with client ID: {sell_client_id}")
sell = self.market_instruction_builder.build_place_order_instructions(
mango.Side.SELL, mango.OrderType.POST_ONLY, ask, sell_size, sell_client_id)
crank = mango.build_mango_consume_events_instructions(
context, self.wallet, model_state.group, model_state.account, model_state.perp_market)
(payer + cancellations + buy + sell + crank).execute(context)
except Exception as exception:
self.logger.error(f"Market-maker error on pulse: {exception} - {traceback.format_exc()}")
settle = self.market_instruction_builder.build_settle_instructions()
def pulse_spot(self, context: mango.Context, model_state: ModelState):
try:
bid, ask = self.calculate_order_prices(model_state)
buy_size, sell_size = self.calculate_order_sizes(model_state)
payer = mango.InstructionData.from_wallet(self.wallet)
srm = context.token_lookup.find_by_symbol("SRM")
if srm is None:
fee_discount_token_account_address = None
else:
fee_discount_token_account = mango.TokenAccount.fetch_largest_for_owner_and_token(
context, self.wallet.address, srm)
fee_discount_token_account_address = fee_discount_token_account.address
cancellations = mango.InstructionData.empty()
for order_id in model_state.spot_open_orders.orders:
if order_id != 0:
side = mango.Side.BUY if order_id in self.buys else mango.Side.SELL
self.logger.info(f"Cancelling order with client ID: {order_id}")
order = mango.Order(id=order_id, client_id=0, owner=self.wallet.address,
side=mango.Side.BUY, price=Decimal(0), size=Decimal(0))
cancel = mango.build_cancel_spot_order_instructions(
context, self.wallet, model_state.group, model_state.account, model_state.spot_market, order, model_state.spot_open_orders.address)
cancellations += cancel
buy_client_id = context.random_client_id()
buy = mango.build_spot_place_order_instructions(context, self.wallet, model_state.group,
model_state.account, model_state.spot_market,
mango.OrderType.LIMIT,
mango.Side.BUY, bid, buy_size, buy_client_id,
fee_discount_token_account_address)
self.buys += [buy_client_id]
self.logger.info(f"Placing BUY order for {buy_size} at price {bid} with client ID: {buy_client_id}")
sell_client_id = context.random_client_id()
sell = mango.build_spot_place_order_instructions(context, self.wallet, model_state.group,
model_state.account, model_state.spot_market,
mango.OrderType.LIMIT,
mango.Side.SELL, ask, sell_size, sell_client_id,
fee_discount_token_account_address)
self.sells += [sell_client_id]
self.logger.info(f"Placing SELL order for {sell_size} at price {ask} with client ID: {sell_client_id}")
open_orders_addresses = list([oo for oo in model_state.account.spot_open_orders if oo is not None])
crank = mango.build_serum_consume_events_instructions(
context, self.wallet, model_state.spot_market, open_orders_addresses)
(payer + cancellations + buy + sell + crank).execute(context)
crank = self.market_instruction_builder.build_crank_instructions()
(payer + cancellations + buy + sell + settle + crank).execute(context)
except Exception as exception:
self.logger.error(f"Market-maker error on pulse: {exception} - {traceback.format_exc()}")

View File

@ -16,19 +16,22 @@
import logging
import mango
import typing
from decimal import Decimal
# # 🥭 ModelState class
#
# Provides simple access to the latest state of market and account data.
#
class ModelState:
def __init__(self, market: mango.Market,
account_watcher: mango.LatestItemObserverSubscriber[mango.Account],
group_watcher: mango.LatestItemObserverSubscriber[mango.Group],
price_watcher: mango.LatestItemObserverSubscriber[mango.Price],
perp_market_watcher: mango.LatestItemObserverSubscriber[mango.PerpMarket],
perp_market_watcher: typing.Optional[mango.LatestItemObserverSubscriber[mango.PerpMarket]],
spot_market_watcher: mango.LatestItemObserverSubscriber[mango.SpotMarket],
spot_open_orders_watcher: mango.LatestItemObserverSubscriber[mango.OpenOrders]
):
@ -37,7 +40,8 @@ class ModelState:
self.account_watcher: mango.LatestItemObserverSubscriber[mango.Account] = account_watcher
self.group_watcher: mango.LatestItemObserverSubscriber[mango.Group] = group_watcher
self.price_watcher: mango.LatestItemObserverSubscriber[mango.Price] = price_watcher
self.perp_market_watcher: mango.LatestItemObserverSubscriber[mango.PerpMarket] = perp_market_watcher
self.perp_market_watcher: typing.Optional[mango.LatestItemObserverSubscriber[mango.PerpMarket]
] = perp_market_watcher
self.spot_market_watcher: mango.LatestItemObserverSubscriber[mango.SpotMarket] = spot_market_watcher
self.spot_open_orders_watcher: mango.LatestItemObserverSubscriber[mango.OpenOrders] = spot_open_orders_watcher
@ -50,7 +54,9 @@ class ModelState:
return self.account_watcher.latest
@property
def perp_market(self) -> mango.PerpMarket:
def perp_market(self) -> typing.Optional[mango.PerpMarket]:
if self.perp_market_watcher is None:
return None
return self.perp_market_watcher.latest
@property
@ -65,6 +71,21 @@ class ModelState:
def price(self) -> mango.Price:
return self.price_watcher.latest
@property
def placed_order_ids(self) -> typing.Sequence[typing.Tuple[Decimal, Decimal]]:
results: typing.List[typing.Tuple[Decimal, Decimal]] = []
if self.spot_open_orders is not None:
for index, order_id in enumerate(self.spot_open_orders.orders):
results += [(order_id, self.spot_open_orders.client_ids[index])]
return results
if self.perp_market is not None:
perp_account = self.account.perp_accounts[self.perp_market.market_index]
for index, order_id in enumerate(perp_account.open_orders.orders):
results += [(order_id, perp_account.open_orders.client_order_ids[index])]
return results
raise Exception("Could not get placed order and client IDs - not a Spot or Perp market.")
def __str__(self) -> str:
return f"""« 𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎 for market '{self.market.symbol}' »"""

View File

@ -56,7 +56,7 @@ class MarketOperations(metaclass=abc.ABCMeta):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@abc.abstractmethod
def cancel_order(self, order: Order) -> str:
def cancel_order(self, order: Order) -> typing.Sequence[str]:
raise NotImplementedError("MarketOperations.cancel_order() is not implemented on the base type.")
@abc.abstractmethod
@ -87,7 +87,7 @@ class NullMarketOperations(MarketOperations):
self.market_name: str = market_name
self.reporter = reporter or (lambda _: None)
def cancel_order(self, order: Order) -> str:
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)

View File

@ -0,0 +1,67 @@
# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
# [Docs](https://docs.mango.markets/)
# [Discord](https://discord.gg/67jySBhxrg)
# [Twitter](https://twitter.com/mangomarkets)
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
from decimal import Decimal
from .account import Account
from .combinableinstructions import CombinableInstructions
from .context import Context
from .group import Group
from .marketinstructionbuilder import MarketInstructionBuilder
from .instructions import build_cancel_perp_order_instructions, build_mango_consume_events_instructions, build_place_perp_order_instructions
from .orders import Order, OrderType, Side
from .perpmarket import PerpMarket
from .wallet import Wallet
# # 🥭 PerpMarketInstructionBuilder
#
# This file deals with building instructions for Perp markets.
#
# As a matter of policy for all InstructionBuidlers, construction and build_* methods should all work with
# existing data, requiring no fetches from Solana or other sources. All necessary data should all be loaded
# on initial setup in the `load()` method.
#
class PerpMarketInstructionBuilder(MarketInstructionBuilder):
def __init__(self, context: Context, wallet: Wallet, group: Group, account: Account, perp_market: PerpMarket):
super().__init__()
self.context: Context = context
self.wallet: Wallet = wallet
self.group: Group = group
self.account: Account = account
self.perp_market: PerpMarket = perp_market
@staticmethod
def load(context: Context, wallet: Wallet, group: Group, account: Account, perp_market: PerpMarket) -> "PerpMarketInstructionBuilder":
return PerpMarketInstructionBuilder(context, wallet, group, account, perp_market)
def build_cancel_order_instructions(self, order: Order) -> CombinableInstructions:
return build_cancel_perp_order_instructions(
self.context, self.wallet, self.account, self.perp_market, order)
def build_place_order_instructions(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal, client_id: int) -> CombinableInstructions:
return build_place_perp_order_instructions(
self.context, self.wallet, self.perp_market.group, self.account, self.perp_market, price, size, client_id, side, order_type)
def build_settle_instructions(self) -> CombinableInstructions:
return CombinableInstructions.empty()
def build_crank_instructions(self, limit: Decimal = Decimal(32)) -> CombinableInstructions:
return build_mango_consume_events_instructions(self.context, self.wallet, self.group, self.account, self.perp_market, limit)
def __str__(self) -> str:
return """« 𝙿𝚎𝚛𝚙𝙼𝚊𝚛𝚔𝚎𝚝𝙸𝚗𝚜𝚝𝚛𝚞𝚌𝚝𝚒𝚘𝚗𝚜 »"""

View File

@ -21,9 +21,10 @@ from solana.publickey import PublicKey
from .account import Account
from .accountinfo import AccountInfo
from .combinableinstructions import CombinableInstructions
from .context import Context
from .marketoperations import MarketOperations
from .instructions import InstructionData, build_cancel_perp_order_instructions, build_place_perp_order_instructions
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
@ -48,17 +49,17 @@ class PerpMarketOperations(MarketOperations):
self.perp_market: PerpMarket = perp_market
self.reporter = reporter or (lambda _: None)
def cancel_order(self, order: Order) -> str:
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)
signers: InstructionData = InstructionData.from_wallet(self.wallet)
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_id(self.context)
return all_instructions.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()
@ -66,7 +67,7 @@ class PerpMarketOperations(MarketOperations):
self.logger.info(report)
self.reporter(report)
signers: InstructionData = InstructionData.from_wallet(self.wallet)
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

View File

@ -0,0 +1,107 @@
# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
# [Docs](https://docs.mango.markets/)
# [Discord](https://discord.gg/67jySBhxrg)
# [Twitter](https://twitter.com/mangomarkets)
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import pyserum.enums
import typing
from decimal import Decimal
from pyserum.market import Market
from solana.publickey import PublicKey
from .combinableinstructions import CombinableInstructions
from .context import Context
from .marketinstructionbuilder import MarketInstructionBuilder
from .openorders import OpenOrders
from .orders import Order, OrderType, Side
from .serummarket import SerumMarket
from .tokenaccount import TokenAccount
from .wallet import Wallet
# # 🥭 SerumMarketInstructionBuilder
#
# This file deals with building instructions for Serum markets.
#
# As a matter of policy for all InstructionBuidlers, construction and build_* methods should all work with
# existing data, requiring no fetches from Solana or other sources. All necessary data should all be loaded
# on initial setup in the `load()` method.
#
class SerumMarketInstructionBuilder(MarketInstructionBuilder):
def __init__(self, context: Context, wallet: Wallet, serum_market: SerumMarket, raw_market: Market, base_token_account: TokenAccount, quote_token_account: TokenAccount, open_orders: OpenOrders, fee_discount_token_address: typing.Optional[PublicKey]):
super().__init__()
self.context: Context = context
self.wallet: Wallet = wallet
self.serum_market: SerumMarket = serum_market
self.raw_market: Market = raw_market
self.base_token_account: TokenAccount = base_token_account
self.quote_token_account: TokenAccount = quote_token_account
self.open_orders: OpenOrders = open_orders
self.fee_discount_token_address: typing.Optional[PublicKey] = fee_discount_token_address
@staticmethod
def load(context: Context, wallet: Wallet, serum_market: SerumMarket) -> "SerumMarketInstructionBuilder":
raw_market: Market = Market.load(context.client, serum_market.address, context.dex_program_id)
fee_discount_token_address: typing.Optional[PublicKey] = None
srm_token = context.token_lookup.find_by_symbol("SRM")
if srm_token is not None:
fee_discount_token_account = TokenAccount.fetch_largest_for_owner_and_token(
context, wallet.address, srm_token)
if fee_discount_token_account is not None:
fee_discount_token_address = fee_discount_token_account.address
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}.")
open_orders = all_open_orders[0]
base_token_account = TokenAccount.fetch_largest_for_owner_and_token(context, wallet.address, serum_market.base)
if base_token_account is None:
raise Exception(f"Could not find source token account for base token {serum_market.base.symbol}.")
quote_token_account = TokenAccount.fetch_largest_for_owner_and_token(
context, wallet.address, serum_market.quote)
if quote_token_account is None:
raise Exception(f"Could not find source token account for quote token {serum_market.quote.symbol}.")
return SerumMarketInstructionBuilder(context, wallet, serum_market, raw_market, base_token_account, quote_token_account, open_orders, fee_discount_token_address)
def build_cancel_order_instructions(self, order: Order) -> CombinableInstructions:
instruction = self.raw_market.make_cancel_order_by_client_id_instruction(
self, self.wallet.account, self.open_orders.address, order.client_id
)
return CombinableInstructions.from_instruction(instruction)
def build_place_order_instructions(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal, client_id: int) -> CombinableInstructions:
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_account = self.quote_token_account if side == Side.BUY else self.base_token_account
instruction = self.raw_market.make_place_order_instruction(payer_token_account.address, self.wallet.account, serum_order_type, serum_side, float(
price), float(size), client_id, self.open_orders.address, self.fee_discount_token_address)
return CombinableInstructions.from_instruction(instruction)
def build_settle_instructions(self) -> CombinableInstructions:
return CombinableInstructions.empty()
def build_crank_instructions(self, limit: Decimal = Decimal(32)) -> CombinableInstructions:
return CombinableInstructions.empty()
def __str__(self) -> str:
return """« 𝚂𝚎𝚛𝚞𝚖𝙼𝚊𝚛𝚔𝚎𝚝𝙸𝚗𝚜𝚝𝚛𝚞𝚌𝚝𝚒𝚘𝚗𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »"""

View File

@ -60,17 +60,17 @@ class SerumMarketOperations(MarketOperations):
else:
self.reporter = just_log
def cancel_order(self, order: Order) -> str:
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)
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 ""
return [""]
def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order:
client_id: int = self.context.random_client_id()

View File

@ -0,0 +1,105 @@
# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
# [Docs](https://docs.mango.markets/)
# [Discord](https://discord.gg/67jySBhxrg)
# [Twitter](https://twitter.com/mangomarkets)
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import typing
from decimal import Decimal
from pyserum.market import Market
from solana.publickey import PublicKey
from .account import Account
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 .marketinstructionbuilder import MarketInstructionBuilder
from .orders import Order, OrderType, Side
from .spotmarket import SpotMarket
from .tokenaccount import TokenAccount
from .wallet import Wallet
# # 🥭 SpotMarketInstructionBuilder
#
# This file deals with building instructions for Spot markets.
#
# As a matter of policy for all InstructionBuidlers, construction and build_* methods should all work with
# existing data, requiring no fetches from Solana or other sources. All necessary data should all be loaded
# on initial setup in the `load()` method.
#
class SpotMarketInstructionBuilder(MarketInstructionBuilder):
def __init__(self, context: Context, wallet: Wallet, group: Group, account: Account, spot_market: SpotMarket, raw_market: Market, base_token_account: TokenAccount, quote_token_account: TokenAccount, market_index: int, fee_discount_token_address: typing.Optional[PublicKey]):
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.raw_market: Market = raw_market
self.base_token_account: TokenAccount = base_token_account
self.quote_token_account: TokenAccount = quote_token_account
self.group_market_index: int = market_index
self.fee_discount_token_address: typing.Optional[PublicKey] = fee_discount_token_address
@staticmethod
def load(context: Context, wallet: Wallet, group: Group, account: Account, spot_market: SpotMarket) -> "SpotMarketInstructionBuilder":
raw_market: Market = Market.load(context.client, spot_market.address, context.dex_program_id)
fee_discount_token_address: typing.Optional[PublicKey] = None
srm_token = context.token_lookup.find_by_symbol("SRM")
if srm_token is not None:
fee_discount_token_account = TokenAccount.fetch_largest_for_owner_and_token(
context, wallet.address, srm_token)
if fee_discount_token_account is not None:
fee_discount_token_address = fee_discount_token_account.address
base_token_account = TokenAccount.fetch_largest_for_owner_and_token(context, wallet.address, spot_market.base)
if base_token_account is None:
raise Exception(f"Could not find source token account for base token {spot_market.base.symbol}.")
quote_token_account = TokenAccount.fetch_largest_for_owner_and_token(
context, wallet.address, spot_market.quote)
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}")
return SpotMarketInstructionBuilder(context, wallet, group, account, spot_market, raw_market, base_token_account, quote_token_account, market_index, fee_discount_token_address)
def build_cancel_order_instructions(self, order: Order) -> CombinableInstructions:
open_orders = self.account.spot_open_orders[self.group_market_index]
return build_cancel_spot_order_instructions(
self.context, self.wallet, self.group, self.account, self.raw_market, order, open_orders)
def build_place_order_instructions(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal, client_id: int) -> CombinableInstructions:
payer_token_account = self.quote_token_account if side == Side.BUY else self.base_token_account
return build_compound_spot_place_order_instructions(
self.context, self.wallet, self.group, self.account, self.raw_market, payer_token_account.address,
order_type, side, price, size, client_id, self.fee_discount_token_address)
def build_settle_instructions(self) -> CombinableInstructions:
return CombinableInstructions.empty()
def build_crank_instructions(self, limit: Decimal = Decimal(32)) -> CombinableInstructions:
return CombinableInstructions.empty()
def __str__(self) -> str:
return """« 𝚂𝚙𝚘𝚝𝙼𝚊𝚛𝚔𝚎𝚝𝙸𝚗𝚜𝚝𝚛𝚞𝚌𝚝𝚒𝚘𝚗𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »"""

View File

@ -25,9 +25,10 @@ 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 InstructionData, build_compound_spot_place_order_instructions, build_cancel_spot_order_instructions
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
@ -93,18 +94,18 @@ class SpotMarketOperations(MarketOperations):
self._serum_fee_discount_token_address_loaded = True
return self._serum_fee_discount_token_address
def cancel_order(self, order: Order) -> str:
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]
signers: InstructionData = InstructionData.from_wallet(self.wallet)
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_id(self.context)
return all_instructions.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
@ -118,7 +119,7 @@ class SpotMarketOperations(MarketOperations):
self.logger.info(report)
self.reporter(report)
signers: InstructionData = InstructionData.from_wallet(self.wallet)
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)

View File

@ -25,8 +25,9 @@ from pyserum.enums import OrderType, Side
from pyserum.market import Market
from solana.publickey import PublicKey
from .combinableinstructions import CombinableInstructions
from .context import Context
from .instructions import InstructionData, build_compound_serum_place_order_instructions, build_create_serum_open_orders_instructions
from .instructions import build_compound_serum_place_order_instructions, build_create_serum_open_orders_instructions
from .retrier import retry_context
from .spotmarket import SpotMarket
from .tokenaccount import TokenAccount
@ -63,11 +64,11 @@ class TradeExecutor(metaclass=abc.ABCMeta):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@abc.abstractmethod
def buy(self, symbol: str, quantity: Decimal) -> str:
def buy(self, symbol: str, quantity: Decimal) -> typing.Sequence[str]:
raise NotImplementedError("TradeExecutor.buy() is not implemented on the base type.")
@abc.abstractmethod
def sell(self, symbol: str, quantity: Decimal) -> str:
def sell(self, symbol: str, quantity: Decimal) -> typing.Sequence[str]:
raise NotImplementedError("TradeExecutor.sell() is not implemented on the base type.")
@ -162,7 +163,7 @@ class SerumImmediateTradeExecutor(TradeExecutor):
self._serum_fee_discount_token_address_loaded = True
return self._serum_fee_discount_token_address
def buy(self, symbol: str, quantity: Decimal) -> str:
def buy(self, symbol: str, quantity: Decimal) -> typing.Sequence[str]:
spot_market = self._lookup_spot_market(symbol)
market = Market.load(self.context.client, spot_market.address)
self.reporter(f"BUY order market: {spot_market.address} {market}")
@ -182,7 +183,7 @@ class SerumImmediateTradeExecutor(TradeExecutor):
quantity
)
def sell(self, symbol: str, quantity: Decimal) -> str:
def sell(self, symbol: str, quantity: Decimal) -> typing.Sequence[str]:
spot_market = self._lookup_spot_market(symbol)
market = Market.load(self.context.client, spot_market.address)
self.reporter(f"SELL order market: {spot_market.address} {market}")
@ -203,8 +204,8 @@ class SerumImmediateTradeExecutor(TradeExecutor):
quantity
)
def _execute(self, spot_market: SpotMarket, market: Market, side: Side, price: Decimal, quantity: Decimal) -> str:
all_instructions: InstructionData = InstructionData.from_wallet(self.wallet)
def _execute(self, spot_market: SpotMarket, market: Market, side: Side, price: Decimal, quantity: Decimal) -> typing.Sequence[str]:
all_instructions: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
base_token_account = TokenAccount.fetch_largest_for_owner_and_token(
self.context, self.wallet.address, spot_market.base)
@ -212,7 +213,7 @@ class SerumImmediateTradeExecutor(TradeExecutor):
create_base_token_account = spl_token.create_associated_token_account(
payer=self.wallet.address, owner=self.wallet.address, mint=spot_market.base.mint
)
all_instructions += InstructionData.from_instruction(create_base_token_account)
all_instructions += CombinableInstructions.from_instruction(create_base_token_account)
base_token_account_address = create_base_token_account.keys[1].pubkey
else:
base_token_account_address = base_token_account.address
@ -223,7 +224,7 @@ class SerumImmediateTradeExecutor(TradeExecutor):
create_quote_token_account = spl_token.create_associated_token_account(
payer=self.wallet.address, owner=self.wallet.address, mint=spot_market.quote.mint
)
all_instructions += InstructionData.from_instruction(create_quote_token_account)
all_instructions += CombinableInstructions.from_instruction(create_quote_token_account)
quote_token_account_address = create_quote_token_account.keys[1].pubkey
else:
quote_token_account_address = quote_token_account.address
@ -247,7 +248,7 @@ class SerumImmediateTradeExecutor(TradeExecutor):
place_order_instructions = build_compound_serum_place_order_instructions(self.context, self.wallet, market, source_token_account_address, open_orders_address,
open_orders_addresses, OrderType.IOC, side, price, quantity, client_id, base_token_account_address, quote_token_account_address, self.serum_fee_discount_token_address)
all_instructions += place_order_instructions
with retry_context("Place Serum Order And Settle", all_instructions.execute_and_unwrap_transaction_id, self.context.retry_pauses) as retrier:
with retry_context("Place Serum Order And Settle", all_instructions.execute_and_unwrap_transaction_ids, self.context.retry_pauses) as retrier:
return retrier.run(self.context)
def _lookup_spot_market(self, symbol: str) -> SpotMarket: