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:
parent
faf514cde1
commit
6a15c81fa3
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}"
|
|
@ -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)
|
||||
|
|
|
@ -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 """« 𝙽𝚞𝚕𝚕𝙼𝚊𝚛𝚔𝚎𝚝𝙸𝚗𝚜𝚝𝚛𝚞𝚌𝚝𝚒𝚘𝚗𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »"""
|
|
@ -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()}")
|
||||
|
||||
|
|
|
@ -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}' »"""
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 """« 𝙿𝚎𝚛𝚙𝙼𝚊𝚛𝚔𝚎𝚝𝙸𝚗𝚜𝚝𝚛𝚞𝚌𝚝𝚒𝚘𝚗𝚜 »"""
|
|
@ -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
|
||||
|
|
|
@ -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 """« 𝚂𝚎𝚛𝚞𝚖𝙼𝚊𝚛𝚔𝚎𝚝𝙸𝚗𝚜𝚝𝚛𝚞𝚌𝚝𝚒𝚘𝚗𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »"""
|
|
@ -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()
|
||||
|
|
|
@ -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 """« 𝚂𝚙𝚘𝚝𝙼𝚊𝚛𝚔𝚎𝚝𝙸𝚗𝚜𝚝𝚛𝚞𝚌𝚝𝚒𝚘𝚗𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »"""
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue