Initial work on placing/cancelling spot market orders.
This commit is contained in:
parent
912b7f574a
commit
3e4ba1052c
|
@ -32,8 +32,10 @@ from .oraclefactory import create_oracle_provider
|
|||
from .perpmarket import PerpMarket
|
||||
from .perpmarketinfo import PerpMarketInfo
|
||||
from .perpmarketoperations import PerpMarketOperations
|
||||
from .perpsmarket import PerpsMarket
|
||||
from .retrier import RetryWithPauses, retry_context
|
||||
from .rootbank import NodeBank, RootBank
|
||||
from .serummarket import SerumMarket
|
||||
from .serummarketlookup import SerumMarketLookup
|
||||
from .serummarketoperations import SerumMarketOperations
|
||||
from .spltokenlookup import SplTokenLookup
|
||||
|
|
|
@ -114,8 +114,8 @@ class Account(AddressableAccount):
|
|||
borrows = ", ".join([f"{borrow}" for borrow in self.borrows])
|
||||
spot_open_orders = ", ".join([f"{oo}" for oo in self.spot_open_orders if oo is not None])
|
||||
perp_accounts = ", ".join(
|
||||
[f"{perp}" for perp in self.perp_accounts if perp.open_orders.is_free_bits != 0xFFFFFFFF])
|
||||
return f"""« 𝙼𝚎𝚛𝚙𝚜𝙰𝚌𝚌𝚘𝚞𝚗𝚝 {self.version} [{self.address}]
|
||||
[f"{perp}".replace("\n", "\n ") for perp in self.perp_accounts if perp.open_orders.is_free_bits != 0xFFFFFFFF])
|
||||
return f"""« 𝙰𝚌𝚌𝚘𝚞𝚗𝚝 {self.version} [{self.address}]
|
||||
{self.meta_data}
|
||||
Owner: {self.owner}
|
||||
Group: {self.group}
|
||||
|
|
|
@ -50,7 +50,7 @@ from .tokenlookup import TokenLookup, CompoundTokenLookup
|
|||
# * GROUP_NAME (defaults to: BTC_ETH_USDT)
|
||||
#
|
||||
|
||||
_default_group_data = MangoConstants["groups"][2]
|
||||
_default_group_data = MangoConstants["groups"][0]
|
||||
default_cluster = os.environ.get("CLUSTER") or _default_group_data["cluster"]
|
||||
default_cluster_url = os.environ.get("CLUSTER_URL") or MangoConstants["cluster_urls"][default_cluster]
|
||||
|
||||
|
@ -84,14 +84,17 @@ class Context:
|
|||
self.transaction_options: TxOpts = TxOpts(preflight_commitment=self.commitment)
|
||||
self.encoding: str = "base64"
|
||||
ids_json_token_lookup: TokenLookup = IdsJsonTokenLookup(cluster, group_name)
|
||||
spl_token_lookup: TokenLookup = SplTokenLookup.load(token_filename)
|
||||
all_token_lookup: TokenLookup = CompoundTokenLookup(
|
||||
[ids_json_token_lookup, spl_token_lookup])
|
||||
all_token_lookup = ids_json_token_lookup
|
||||
if cluster == "mainnet-beta":
|
||||
spl_token_lookup: TokenLookup = SplTokenLookup.load(token_filename)
|
||||
all_token_lookup = CompoundTokenLookup([ids_json_token_lookup, spl_token_lookup])
|
||||
self.token_lookup: TokenLookup = all_token_lookup
|
||||
|
||||
ids_json_market_lookup: MarketLookup = IdsJsonMarketLookup(cluster)
|
||||
serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(token_filename)
|
||||
all_market_lookup = CompoundMarketLookup([ids_json_market_lookup, serum_market_lookup])
|
||||
all_market_lookup = ids_json_market_lookup
|
||||
if cluster == "mainnet-beta":
|
||||
serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(token_filename)
|
||||
all_market_lookup = CompoundMarketLookup([ids_json_market_lookup, serum_market_lookup])
|
||||
self.market_lookup: MarketLookup = all_market_lookup
|
||||
|
||||
# kangda said in Discord: https://discord.com/channels/791995070613159966/836239696467591186/847816026245693451
|
||||
|
|
|
@ -23,8 +23,11 @@ from .market import Market
|
|||
from .marketoperations import MarketOperations, NullMarketOperations
|
||||
from .perpmarket import PerpMarket
|
||||
from .perpmarketoperations import PerpMarketOperations
|
||||
# from .serummarketoperations import SerumMarketOperations
|
||||
from .perpsmarket import PerpsMarket
|
||||
from .serummarket import SerumMarket
|
||||
from .serummarketoperations import SerumMarketOperations
|
||||
from .spotmarket import SpotMarket
|
||||
from .spotmarketoperations import SpotMarketOperations
|
||||
from .wallet import Wallet
|
||||
|
||||
# # 🥭 create_market_operations
|
||||
|
@ -35,9 +38,13 @@ from .wallet import Wallet
|
|||
def create_market_operations(context: Context, wallet: Wallet, dry_run: bool, market: Market, reporter: typing.Callable[[str], None]) -> MarketOperations:
|
||||
if dry_run:
|
||||
return NullMarketOperations(market.symbol, reporter)
|
||||
elif isinstance(market, SerumMarket):
|
||||
return SerumMarketOperations(context, wallet, market, reporter)
|
||||
elif isinstance(market, SpotMarket):
|
||||
# return SerumMarketOperations(context, wallet, market, reporter)
|
||||
# elif isinstance(market, PerpMarket):
|
||||
group = Group.load(context, market.group_address)
|
||||
margin_accounts = Account.load_all_for_owner(context, wallet.address, group)
|
||||
return SpotMarketOperations(context, wallet, group, margin_accounts[0], market, reporter)
|
||||
elif isinstance(market, PerpsMarket):
|
||||
group = Group.load(context, context.group_id)
|
||||
margin_accounts = Account.load_all_for_owner(context, wallet.address, group)
|
||||
perp_market_info = group.perp_markets[0]
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# [Github](https://github.com/blockworks-foundation)
|
||||
# [Email](mailto:hello@blockworks.foundation)
|
||||
|
||||
|
||||
import enum
|
||||
import typing
|
||||
|
||||
from decimal import Decimal
|
||||
|
@ -22,10 +22,22 @@ from solana.publickey import PublicKey
|
|||
from .constants import MangoConstants
|
||||
from .market import Market
|
||||
from .marketlookup import MarketLookup
|
||||
from .perpsmarket import PerpsMarket
|
||||
from .spotmarket import SpotMarket
|
||||
from .token import Token
|
||||
|
||||
|
||||
class IdsJsonMarketType(enum.Enum):
|
||||
PERP = enum.auto()
|
||||
SPOT = enum.auto()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self}"
|
||||
|
||||
|
||||
# # 🥭 IdsJsonMarketLookup class
|
||||
#
|
||||
# This class allows us to look up market data from our ids.json configuration file.
|
||||
|
@ -38,7 +50,7 @@ class IdsJsonMarketLookup(MarketLookup):
|
|||
self.cluster: str = cluster
|
||||
|
||||
@staticmethod
|
||||
def _from_dict(data: typing.Dict, tokens: typing.Sequence[Token], quote_symbol: str) -> Market:
|
||||
def _from_dict(market_type: IdsJsonMarketType, group_address: PublicKey, data: typing.Dict, tokens: typing.Sequence[Token], quote_symbol: str) -> Market:
|
||||
base_symbol = data["baseSymbol"]
|
||||
if tokens[0].symbol == quote_symbol and tokens[1].symbol == base_symbol:
|
||||
quote = tokens[0]
|
||||
|
@ -50,7 +62,10 @@ class IdsJsonMarketLookup(MarketLookup):
|
|||
raise Exception(f"Could not find base ('{base_symbol}') or quote symbol ('{quote_symbol}') in tokens.")
|
||||
|
||||
address = PublicKey(data["publicKey"])
|
||||
return SpotMarket(base, quote, address)
|
||||
if market_type == IdsJsonMarketType.PERP:
|
||||
return PerpsMarket(base, quote, address)
|
||||
else:
|
||||
return SpotMarket(base, quote, address, group_address)
|
||||
|
||||
@staticmethod
|
||||
def _load_tokens(data: typing.Dict) -> typing.Sequence[Token]:
|
||||
|
@ -64,28 +79,45 @@ class IdsJsonMarketLookup(MarketLookup):
|
|||
def find_by_symbol(self, symbol: str) -> typing.Optional[Market]:
|
||||
for group in MangoConstants["groups"]:
|
||||
if group["cluster"] == self.cluster:
|
||||
group_address: PublicKey = PublicKey(group["publicKey"])
|
||||
for market_data in group["perpMarkets"]:
|
||||
if market_data["name"].upper() == symbol.upper():
|
||||
tokens = IdsJsonMarketLookup._load_tokens(group["tokens"])
|
||||
return IdsJsonMarketLookup._from_dict(market_data, tokens, group["quoteSymbol"])
|
||||
return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, group_address, market_data, tokens, group["quoteSymbol"])
|
||||
for market_data in group["spotMarkets"]:
|
||||
if market_data["name"].upper() == symbol.upper():
|
||||
tokens = IdsJsonMarketLookup._load_tokens(group["tokens"])
|
||||
return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.SPOT, group_address, market_data, tokens, group["quoteSymbol"])
|
||||
return None
|
||||
|
||||
def find_by_address(self, address: PublicKey) -> typing.Optional[Market]:
|
||||
for group in MangoConstants["groups"]:
|
||||
if group["cluster"] == self.cluster:
|
||||
for spot_market in group["spotMarkets"]:
|
||||
if spot_market["key"] == str(address):
|
||||
group_address: PublicKey = PublicKey(group["publicKey"])
|
||||
for market_data in group["perpMarkets"]:
|
||||
if market_data["key"] == str(address):
|
||||
tokens = IdsJsonMarketLookup._load_tokens(group["tokens"])
|
||||
return IdsJsonMarketLookup._from_dict(spot_market, tokens, group["quoteSymbol"])
|
||||
return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, group_address, market_data, tokens, group["quoteSymbol"])
|
||||
for market_data in group["spotMarkets"]:
|
||||
if market_data["key"] == str(address):
|
||||
tokens = IdsJsonMarketLookup._load_tokens(group["tokens"])
|
||||
return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.SPOT, group_address, market_data, tokens, group["quoteSymbol"])
|
||||
return None
|
||||
|
||||
def all_markets(self) -> typing.Sequence[Market]:
|
||||
markets = []
|
||||
for group in MangoConstants["groups"]:
|
||||
if group["cluster"] == self.cluster:
|
||||
for spot_market in group["spotMarkets"]:
|
||||
group_address: PublicKey = PublicKey(group["publicKey"])
|
||||
for market_data in group["perpMarkets"]:
|
||||
tokens = IdsJsonMarketLookup._load_tokens(group["tokens"])
|
||||
market = IdsJsonMarketLookup._from_dict(spot_market, tokens, group["quoteSymbol"])
|
||||
market = IdsJsonMarketLookup._from_dict(
|
||||
IdsJsonMarketType.PERP, group_address, market_data, tokens, group["quoteSymbol"])
|
||||
markets = [market]
|
||||
for market_data in group["spotMarkets"]:
|
||||
tokens = IdsJsonMarketLookup._load_tokens(group["tokens"])
|
||||
market = IdsJsonMarketLookup._from_dict(
|
||||
IdsJsonMarketType.SPOT, group_address, market_data, tokens, group["quoteSymbol"])
|
||||
markets = [market]
|
||||
|
||||
return markets
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# [Email](mailto:hello@blockworks.foundation)
|
||||
|
||||
|
||||
import pyserum.enums
|
||||
import typing
|
||||
|
||||
from decimal import Decimal
|
||||
|
@ -24,7 +25,7 @@ from pyserum.open_orders_account import make_create_account_instruction
|
|||
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
|
||||
from solana.sysvar import SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY
|
||||
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
|
||||
|
@ -145,6 +146,7 @@ def build_serum_consume_events_instructions(context: Context, wallet: Wallet, ma
|
|||
market=market.state.public_key(),
|
||||
event_queue=market.state.event_queue(),
|
||||
open_orders_accounts=open_orders_addresses,
|
||||
program_id=context.dex_program_id,
|
||||
limit=limit
|
||||
))
|
||||
|
||||
|
@ -369,9 +371,202 @@ def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group,
|
|||
# pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in margin_account.spot_open_orders])
|
||||
],
|
||||
program_id=context.program_id,
|
||||
data=layouts.WITHDRAW_V3.build({
|
||||
data=layouts.WITHDRAW.build({
|
||||
"quantity": value,
|
||||
"allow_borrow": allow_borrow
|
||||
})
|
||||
)
|
||||
return [withdraw]
|
||||
|
||||
|
||||
# # 🥭 build_mango_place_order_instructions function
|
||||
#
|
||||
# Creates a Mango order-placing instruction using the Serum instruction as the inner instruction.
|
||||
#
|
||||
|
||||
def build_spot_place_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account,
|
||||
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]) -> typing.Sequence[TransactionInstruction]:
|
||||
|
||||
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
|
||||
intrinsic_price = market.state.price_number_to_lots(float(price))
|
||||
max_base_quantity = market.state.base_size_number_to_lots(float(quantity))
|
||||
max_quote_quantity = market.state.base_size_number_to_lots(
|
||||
float(quantity)) * market.state.quote_lot_size() * market.state.price_number_to_lots(float(price))
|
||||
|
||||
quote_token_info = group.shared_quote_token
|
||||
base_token_infos = [
|
||||
token_info for token_info in group.base_tokens if token_info is not None and token_info.token.mint == market.state.base_mint()]
|
||||
if len(base_token_infos) != 1:
|
||||
raise Exception(
|
||||
f"Could not find base token info for group {group.address} - length was {len(base_token_infos)} when it should be 1.")
|
||||
base_token_info = base_token_infos[0]
|
||||
|
||||
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(),
|
||||
)
|
||||
|
||||
base_root_bank = RootBank.load(context, base_token_info.root_bank)
|
||||
base_node_bank = base_root_bank.pick_node_bank(context)
|
||||
quote_root_bank = RootBank.load(context, quote_token_info.root_bank)
|
||||
quote_node_bank = quote_root_bank.pick_node_bank(context)
|
||||
|
||||
# /// Accounts expected by this instruction (22+openorders):
|
||||
# { isSigner: false, isWritable: false, pubkey: mangoGroupPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: mangoAccountPk },
|
||||
# { isSigner: true, isWritable: false, pubkey: ownerPk },
|
||||
# { isSigner: false, isWritable: false, pubkey: mangoCachePk },
|
||||
# { isSigner: false, isWritable: false, pubkey: serumDexPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: spotMarketPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: bidsPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: asksPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: requestQueuePk },
|
||||
# { isSigner: false, isWritable: true, pubkey: eventQueuePk },
|
||||
# { isSigner: false, isWritable: true, pubkey: spotMktBaseVaultPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: spotMktQuoteVaultPk },
|
||||
# { isSigner: false, isWritable: false, pubkey: baseRootBankPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: baseNodeBankPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: quoteRootBankPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: quoteNodeBankPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: quoteVaultPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: baseVaultPk },
|
||||
# { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
|
||||
# { isSigner: false, isWritable: false, pubkey: signerPk },
|
||||
# { isSigner: false, isWritable: false, pubkey: SYSVAR_RENT_PUBKEY },
|
||||
# { isSigner: false, isWritable: false, pubkey: dexSignerPk },
|
||||
# ...openOrders.map((pubkey) => ({
|
||||
# isSigner: false,
|
||||
# isWritable: true, // TODO: only pass the one writable you are going to place the order on
|
||||
# pubkey,
|
||||
# })),
|
||||
fee_discount_address_meta: typing.List[AccountMeta] = []
|
||||
if fee_discount_address is not None:
|
||||
fee_discount_address_meta = [AccountMeta(is_signer=False, is_writable=False, pubkey=fee_discount_address)]
|
||||
instructions = [
|
||||
TransactionInstruction(
|
||||
keys=[
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=group.address),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=account.address),
|
||||
AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=group.cache),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=context.dex_program_id),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.public_key()),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.bids()),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.asks()),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.request_queue()),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.event_queue()),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.base_vault()),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.quote_vault()),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=base_root_bank.address),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=base_node_bank.address),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=quote_root_bank.address),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=quote_node_bank.address),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=quote_node_bank.vault),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=base_node_bank.vault),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=SYSVAR_RENT_PUBKEY),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=vault_signer),
|
||||
*list([AccountMeta(is_signer=False, is_writable=(oo_address == open_orders_address),
|
||||
pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in account.spot_open_orders]),
|
||||
*fee_discount_address_meta
|
||||
],
|
||||
program_id=context.program_id,
|
||||
data=layouts.PLACE_SPOT_ORDER.build(
|
||||
dict(
|
||||
side=serum_side,
|
||||
limit_price=intrinsic_price,
|
||||
max_base_quantity=max_base_quantity,
|
||||
max_quote_quantity=max_quote_quantity,
|
||||
self_trade_behavior=pyserum.enums.SelfTradeBehavior.DECREMENT_TAKE,
|
||||
order_type=serum_order_type,
|
||||
client_id=client_id,
|
||||
limit=65535,
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
return instructions
|
||||
|
||||
|
||||
def build_compound_spot_place_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account,
|
||||
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]) -> typing.Sequence[TransactionInstruction]:
|
||||
place_order = build_spot_place_order_instructions(
|
||||
context, wallet, group, account, market, source, open_orders_address, order_type, side, price, quantity, client_id, fee_discount_address)
|
||||
open_order_accounts = market.find_open_orders_accounts_for_owner(wallet.address)
|
||||
open_order_addresses = list(oo.address for oo in open_order_accounts)
|
||||
consume_events = build_serum_consume_events_instructions(context, wallet, market, open_order_addresses)
|
||||
|
||||
quote_token_info = group.shared_quote_token
|
||||
base_token_infos = [
|
||||
token_info for token_info in group.base_tokens if token_info is not None and token_info.token.mint == market.state.base_mint()]
|
||||
if len(base_token_infos) != 1:
|
||||
raise Exception(
|
||||
f"Could not find base token info for group {group.address} - length was {len(base_token_infos)} when it should be 1.")
|
||||
base_token_info = base_token_infos[0]
|
||||
base_token_account = TokenAccount.fetch_largest_for_owner_and_token(context, wallet.address, base_token_info.token)
|
||||
quote_token_account = TokenAccount.fetch_largest_for_owner_and_token(
|
||||
context, wallet.address, quote_token_info.token)
|
||||
|
||||
settle: typing.Sequence[TransactionInstruction] = []
|
||||
if base_token_account is not None and quote_token_account is not None:
|
||||
settlement_open_orders = [oo for oo in open_order_accounts if oo.market == market.state.public_key()]
|
||||
if len(settlement_open_orders) > 0 and settlement_open_orders[0] is not None:
|
||||
settle = build_serum_settle_instructions(
|
||||
context, wallet, market, settlement_open_orders[0].address, base_token_account.address, quote_token_account.address)
|
||||
|
||||
return [*place_order, *consume_events, *settle]
|
||||
|
||||
|
||||
# # 🥭 build_cancel_perp_order_instruction function
|
||||
#
|
||||
# Builds the instructions necessary for cancelling a perp order.
|
||||
#
|
||||
|
||||
|
||||
def build_cancel_spot_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, market: Market, order: Order, open_orders_address: PublicKey) -> typing.Sequence[TransactionInstruction]:
|
||||
# { buy: 0, sell: 1 }
|
||||
raw_side: int = 1 if order.side == Side.SELL else 0
|
||||
|
||||
# Accounts expected by this instruction:
|
||||
# { isSigner: false, isWritable: false, pubkey: mangoGroupPk },
|
||||
# { isSigner: true, isWritable: false, pubkey: ownerPk },
|
||||
# { isSigner: false, isWritable: false, pubkey: mangoAccountPk },
|
||||
# { isSigner: false, isWritable: false, pubkey: dexProgramId },
|
||||
# { isSigner: false, isWritable: true, pubkey: spotMarketPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: bidsPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: asksPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: openOrdersPk },
|
||||
# { isSigner: false, isWritable: false, pubkey: signerKey },
|
||||
# { isSigner: false, isWritable: true, pubkey: eventQueuePk },
|
||||
|
||||
return [
|
||||
TransactionInstruction(
|
||||
keys=[
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=group.address),
|
||||
AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=account.address),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=context.dex_program_id),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.public_key()),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.bids()),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.asks()),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=open_orders_address),
|
||||
AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key),
|
||||
AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.event_queue())
|
||||
],
|
||||
program_id=context.program_id,
|
||||
data=layouts.CANCEL_SPOT_ORDER.build(
|
||||
{
|
||||
"order_id": order.id,
|
||||
"side": raw_side
|
||||
})
|
||||
)
|
||||
]
|
||||
|
|
|
@ -600,7 +600,7 @@ CANCEL_PERP_ORDER = construct.Struct(
|
|||
# quantity: u64,
|
||||
# allow_borrow: bool,
|
||||
# },
|
||||
WITHDRAW_V3 = construct.Struct(
|
||||
WITHDRAW = construct.Struct(
|
||||
"variant" / construct.Const(14, construct.BytesInteger(4, swapped=True)),
|
||||
|
||||
"quantity" / DecimalAdapter(),
|
||||
|
@ -608,17 +608,74 @@ WITHDRAW_V3 = construct.Struct(
|
|||
)
|
||||
|
||||
|
||||
# /// Place an order on the Serum Dex using Mango account
|
||||
# /// Accounts expected by this instruction (22+openorders):
|
||||
# { isSigner: false, isWritable: false, pubkey: mangoGroupPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: mangoAccountPk },
|
||||
# { isSigner: true, isWritable: false, pubkey: ownerPk },
|
||||
# { isSigner: false, isWritable: false, pubkey: mangoCachePk },
|
||||
# { isSigner: false, isWritable: false, pubkey: serumDexPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: spotMarketPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: bidsPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: asksPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: requestQueuePk },
|
||||
# { isSigner: false, isWritable: true, pubkey: eventQueuePk },
|
||||
# { isSigner: false, isWritable: true, pubkey: spotMktBaseVaultPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: spotMktQuoteVaultPk },
|
||||
# { isSigner: false, isWritable: false, pubkey: baseRootBankPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: baseNodeBankPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: quoteRootBankPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: quoteNodeBankPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: quoteVaultPk },
|
||||
# { isSigner: false, isWritable: true, pubkey: baseVaultPk },
|
||||
# { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
|
||||
# { isSigner: false, isWritable: false, pubkey: signerPk },
|
||||
# { isSigner: false, isWritable: false, pubkey: SYSVAR_RENT_PUBKEY },
|
||||
# { isSigner: false, isWritable: false, pubkey: dexSignerPk },
|
||||
# ...openOrders.map((pubkey) => ({
|
||||
# isSigner: false,
|
||||
# isWritable: true, // TODO: only pass the one writable you are going to place the order on
|
||||
# pubkey,
|
||||
# })),
|
||||
PLACE_SPOT_ORDER = construct.Struct(
|
||||
"variant" / construct.Const(9, construct.BytesInteger(4, swapped=True)), # 4
|
||||
|
||||
'side' / DecimalAdapter(4), # 8
|
||||
"limit_price" / DecimalAdapter(), # 16
|
||||
'max_base_quantity' / DecimalAdapter(), # 24
|
||||
'max_quote_quantity' / DecimalAdapter(), # 32
|
||||
'self_trade_behavior' / DecimalAdapter(4), # 36
|
||||
'order_type' / DecimalAdapter(4), # 40
|
||||
'client_id' / DecimalAdapter(), # 48
|
||||
'limit' / DecimalAdapter(2), # 50
|
||||
)
|
||||
|
||||
# /// Cancel an order using dex instruction
|
||||
# ///
|
||||
# /// Accounts expected by this instruction ():
|
||||
# ///
|
||||
# CancelSpotOrder {
|
||||
# order: serum_dex::instruction::CancelOrderInstructionV2,
|
||||
# },
|
||||
CANCEL_SPOT_ORDER = construct.Struct(
|
||||
"variant" / construct.Const(20, construct.BytesInteger(4, swapped=True)),
|
||||
|
||||
'side' / DecimalAdapter(4),
|
||||
"order_id" / DecimalAdapter(16)
|
||||
)
|
||||
|
||||
|
||||
InstructionParsersByVariant = {
|
||||
0: None, # INIT_MANGO_GROUP,
|
||||
1: INIT_MANGO_ACCOUNT, # INIT_MANGO_ACCOUNT,
|
||||
2: None, # DEPOSIT,
|
||||
3: WITHDRAW_V3, # WITHDRAW,
|
||||
3: WITHDRAW, # WITHDRAW,
|
||||
4: None, # ADD_SPOT_MARKET,
|
||||
5: None, # ADD_TO_BASKET,
|
||||
6: None, # BORROW,
|
||||
7: None, # CACHE_PRICES,
|
||||
8: None, # CACHE_ROOT_BANKS,
|
||||
9: None, # PLACE_SPOT_ORDER,
|
||||
9: PLACE_SPOT_ORDER, # PLACE_SPOT_ORDER,
|
||||
10: None, # ADD_ORACLE,
|
||||
11: None, # ADD_PERP_MARKET,
|
||||
12: PLACE_PERP_ORDER, # PLACE_PERP_ORDER,
|
||||
|
@ -629,7 +686,7 @@ InstructionParsersByVariant = {
|
|||
17: None, # UPDATE_FUNDING,
|
||||
18: None, # SET_ORACLE,
|
||||
19: None, # SETTLE_FUNDS,
|
||||
20: None, # CANCEL_SPOT_ORDER,
|
||||
20: CANCEL_SPOT_ORDER, # CANCEL_SPOT_ORDER,
|
||||
21: None, # UPDATE_ROOT_BANK,
|
||||
22: None, # SETTLE_PNL,
|
||||
23: None, # SETTLE_BORROW,
|
||||
|
|
|
@ -36,7 +36,7 @@ class Market(metaclass=abc.ABCMeta):
|
|||
return f"{self.base.symbol}/{self.quote.symbol}"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"« Market {self.symbol} »"
|
||||
return f"« 𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} »"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self}"
|
||||
|
|
|
@ -128,8 +128,8 @@ class OpenOrders(AddressableAccount):
|
|||
)
|
||||
]
|
||||
|
||||
response = context.client.get_program_accounts(
|
||||
context.dex_program_id, data_size=layouts.OPEN_ORDERS.sizeof(), memcmp_opts=filters, commitment=context.commitment, encoding="base64")
|
||||
response = context.client.get_program_accounts(program_id, data_size=layouts.OPEN_ORDERS.sizeof(
|
||||
), memcmp_opts=filters, commitment=context.commitment, encoding="base64")
|
||||
accounts = list(map(lambda pair: AccountInfo._from_response_values(pair[0], pair[1]), [
|
||||
(result["account"], PublicKey(result["pubkey"])) for result in response["result"]]))
|
||||
return list(map(lambda acc: OpenOrders.parse(acc, base_decimals, quote_decimals), accounts))
|
||||
|
|
|
@ -15,10 +15,12 @@
|
|||
|
||||
|
||||
import enum
|
||||
import pyserum.enums
|
||||
|
||||
import typing
|
||||
|
||||
from decimal import Decimal
|
||||
from pyserum.market.types import Order as SerumOrder
|
||||
from solana.publickey import PublicKey
|
||||
|
||||
|
||||
|
@ -79,3 +81,12 @@ class Order(typing.NamedTuple):
|
|||
side: Side
|
||||
price: Decimal
|
||||
size: Decimal
|
||||
|
||||
@staticmethod
|
||||
def from_serum_order(serum_order: SerumOrder) -> "Order":
|
||||
price = Decimal(serum_order.info.price)
|
||||
size = Decimal(serum_order.info.size)
|
||||
side = Side.BUY if serum_order.side == pyserum.enums.Side.BUY else Side.SELL
|
||||
order = Order(id=serum_order.order_id, side=side, price=price, size=size,
|
||||
client_id=serum_order.client_id, owner=serum_order.open_order_address)
|
||||
return order
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
# # ⚠ 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 solana.publickey import PublicKey
|
||||
|
||||
from .market import Market
|
||||
from .token import Token
|
||||
|
||||
|
||||
# # 🥭 PerpsMarket class
|
||||
#
|
||||
# This class encapsulates our knowledge of a Mango perps market.
|
||||
#
|
||||
|
||||
|
||||
class PerpsMarket(Market):
|
||||
def __init__(self, base: Token, quote: Token, address: PublicKey):
|
||||
super().__init__(base, quote)
|
||||
self.address: PublicKey = address
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"« 𝙿𝚎𝚛𝚙𝚜𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol}: {self.address} »"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self}"
|
|
@ -0,0 +1,38 @@
|
|||
# # ⚠ 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 solana.publickey import PublicKey
|
||||
|
||||
from .market import Market
|
||||
from .token import Token
|
||||
|
||||
|
||||
# # 🥭 SerumMarket class
|
||||
#
|
||||
# This class encapsulates our knowledge of a Serum spot market.
|
||||
#
|
||||
|
||||
|
||||
class SerumMarket(Market):
|
||||
def __init__(self, base: Token, quote: Token, address: PublicKey):
|
||||
super().__init__(base, quote)
|
||||
self.address: PublicKey = address
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"« 𝚂𝚎𝚛𝚞𝚖𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol}: {self.address} »"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self}"
|
|
@ -22,7 +22,7 @@ from solana.publickey import PublicKey
|
|||
|
||||
from .market import Market
|
||||
from .marketlookup import MarketLookup
|
||||
from .spotmarket import SpotMarket
|
||||
from .serummarket import SerumMarket
|
||||
from .token import Token
|
||||
|
||||
|
||||
|
@ -119,7 +119,7 @@ class SerumMarketLookup(MarketLookup):
|
|||
f"Could not find market with quote token '{quote.symbol}'. Only markets based on USDC or USDT are supported.")
|
||||
return None
|
||||
|
||||
return SpotMarket(base, quote, market_address)
|
||||
return SerumMarket(base, quote, market_address)
|
||||
|
||||
def find_by_address(self, address: PublicKey) -> typing.Optional[Market]:
|
||||
address_string: str = str(address)
|
||||
|
@ -136,7 +136,7 @@ class SerumMarketLookup(MarketLookup):
|
|||
raise Exception("Could not load token data for USDC (which should always be present).")
|
||||
quote = Token(quote_data["symbol"], quote_data["name"], PublicKey(
|
||||
quote_data["address"]), Decimal(quote_data["decimals"]))
|
||||
return SpotMarket(base, quote, market_address)
|
||||
return SerumMarket(base, quote, market_address)
|
||||
if "serumV3Usdt" in token_data["extensions"]:
|
||||
if token_data["extensions"]["serumV3Usdt"] == address_string:
|
||||
market_address_string = token_data["extensions"]["serumV3Usdt"]
|
||||
|
@ -148,14 +148,14 @@ class SerumMarketLookup(MarketLookup):
|
|||
raise Exception("Could not load token data for USDT (which should always be present).")
|
||||
quote = Token(quote_data["symbol"], quote_data["name"], PublicKey(
|
||||
quote_data["address"]), Decimal(quote_data["decimals"]))
|
||||
return SpotMarket(base, quote, market_address)
|
||||
return SerumMarket(base, quote, market_address)
|
||||
return None
|
||||
|
||||
def all_markets(self) -> typing.Sequence[Market]:
|
||||
usdt = SerumMarketLookup._find_token_by_symbol_or_error("USDT", self.token_data)
|
||||
usdc = SerumMarketLookup._find_token_by_symbol_or_error("USDC", self.token_data)
|
||||
|
||||
all_markets: typing.List[SpotMarket] = []
|
||||
all_markets: typing.List[SerumMarket] = []
|
||||
for token_data in self.token_data["tokens"]:
|
||||
if "extensions" in token_data:
|
||||
if "serumV3Usdc" in token_data["extensions"]:
|
||||
|
@ -163,12 +163,12 @@ class SerumMarketLookup(MarketLookup):
|
|||
market_address = PublicKey(market_address_string)
|
||||
base = Token(token_data["symbol"], token_data["name"], PublicKey(
|
||||
token_data["address"]), Decimal(token_data["decimals"]))
|
||||
all_markets += [SpotMarket(base, usdc, market_address)]
|
||||
all_markets += [SerumMarket(base, usdc, market_address)]
|
||||
if "serumV3Usdt" in token_data["extensions"]:
|
||||
market_address_string = token_data["extensions"]["serumV3Usdt"]
|
||||
market_address = PublicKey(market_address_string)
|
||||
base = Token(token_data["symbol"], token_data["name"], PublicKey(
|
||||
token_data["address"]), Decimal(token_data["decimals"]))
|
||||
all_markets += [SpotMarket(base, usdt, market_address)]
|
||||
all_markets += [SerumMarket(base, usdt, market_address)]
|
||||
|
||||
return all_markets
|
||||
|
|
|
@ -19,14 +19,13 @@ import typing
|
|||
|
||||
from decimal import Decimal
|
||||
from pyserum.market import Market
|
||||
from pyserum.market.types import Order as SerumOrder
|
||||
from solana.rpc.types import TxOpts
|
||||
|
||||
from .context import Context
|
||||
from .marketoperations import MarketOperations
|
||||
from .openorders import OpenOrders
|
||||
from .orders import Order, OrderType, Side
|
||||
from .spotmarket import SpotMarket
|
||||
from .serummarket import SerumMarket
|
||||
from .tokenaccount import TokenAccount
|
||||
from .wallet import Wallet
|
||||
|
||||
|
@ -37,16 +36,16 @@ from .wallet import Wallet
|
|||
#
|
||||
|
||||
class SerumMarketOperations(MarketOperations):
|
||||
def __init__(self, context: Context, wallet: Wallet, spot_market: SpotMarket, reporter: typing.Callable[[str], None] = None):
|
||||
def __init__(self, context: Context, wallet: Wallet, serum_market: SerumMarket, reporter: typing.Callable[[str], None] = None):
|
||||
super().__init__()
|
||||
self.context: Context = context
|
||||
self.wallet: Wallet = wallet
|
||||
self.spot_market: SpotMarket = spot_market
|
||||
self.market: Market = Market.load(context.client, spot_market.address, context.dex_program_id)
|
||||
self.serum_market: SerumMarket = serum_market
|
||||
self.market: Market = Market.load(context.client, serum_market.address, context.dex_program_id)
|
||||
all_open_orders = OpenOrders.load_for_market_and_owner(
|
||||
context, spot_market.address, wallet.address, context.dex_program_id, spot_market.base.decimals, spot_market.quote.decimals)
|
||||
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 {spot_market}.")
|
||||
raise Exception(f"No OpenOrders account available for market {serum_market}.")
|
||||
self.open_orders = all_open_orders[0]
|
||||
|
||||
def report(text):
|
||||
|
@ -63,7 +62,7 @@ class SerumMarketOperations(MarketOperations):
|
|||
|
||||
def cancel_order(self, order: Order) -> str:
|
||||
self.reporter(
|
||||
f"Cancelling order {order.id} in openorders {self.open_orders.address} on market {self.spot_market.symbol}.")
|
||||
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,
|
||||
|
@ -75,12 +74,12 @@ class SerumMarketOperations(MarketOperations):
|
|||
|
||||
def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order:
|
||||
client_id: int = self.context.random_client_id()
|
||||
report: str = f"Placing {order_type} {side} order for size {size} at price {price} on market {self.spot_market.symbol} with ID {client_id}."
|
||||
report: str = f"Placing {order_type} {side} order for size {size} at price {price} on market {self.serum_market.symbol} with ID {client_id}."
|
||||
self.logger.info(report)
|
||||
self.reporter(report)
|
||||
serum_order_type = pyserum.enums.OrderType.POST_ONLY if order_type == OrderType.POST_ONLY else pyserum.enums.OrderType.IOC if order_type == OrderType.IOC else pyserum.enums.OrderType.LIMIT
|
||||
serum_side = pyserum.enums.Side.BUY if side == Side.BUY else pyserum.enums.Side.SELL
|
||||
payer_token = self.spot_market.quote if side == Side.BUY else self.spot_market.base
|
||||
payer_token = self.serum_market.quote if side == Side.BUY else self.serum_market.base
|
||||
token_account = TokenAccount.fetch_largest_for_owner_and_token(self.context, self.wallet.address, payer_token)
|
||||
if token_account is None:
|
||||
raise Exception(f"Could not find payer token account for token {payer_token.symbol}.")
|
||||
|
@ -91,23 +90,15 @@ class SerumMarketOperations(MarketOperations):
|
|||
self.context.unwrap_or_raise_exception(response)
|
||||
return Order(id=0, side=side, price=price, size=size, client_id=client_id, owner=self.open_orders.address)
|
||||
|
||||
def _serum_order_to_order(serum_order: SerumOrder) -> Order:
|
||||
price = Decimal(serum_order.info.price)
|
||||
size = Decimal(serum_order.info.size)
|
||||
side = Side.BUY if serum_order.side == pyserum.enums.Side.BUY else Side.SELL
|
||||
order = Order(id=serum_order.order_id, side=side, price=price, size=size,
|
||||
client_id=serum_order.client_id, owner=serum_order.open_order_address)
|
||||
return order
|
||||
|
||||
def load_orders(self) -> typing.Sequence[Order]:
|
||||
asks = self.market.load_asks()
|
||||
orders: typing.List[Order] = []
|
||||
for serum_order in asks:
|
||||
orders += [SerumMarketOperations._serum_order_to_order(serum_order)]
|
||||
orders += [Order.from_serum_order(serum_order)]
|
||||
|
||||
bids = self.market.load_bids()
|
||||
for serum_order in bids:
|
||||
orders += [SerumMarketOperations._serum_order_to_order(serum_order)]
|
||||
orders += [Order.from_serum_order(serum_order)]
|
||||
|
||||
return orders
|
||||
|
||||
|
@ -115,9 +106,9 @@ class SerumMarketOperations(MarketOperations):
|
|||
serum_orders = self.market.load_orders_for_owner(self.wallet.address)
|
||||
orders: typing.List[Order] = []
|
||||
for serum_order in serum_orders:
|
||||
orders += [SerumMarketOperations._serum_order_to_order(serum_order)]
|
||||
orders += [Order.from_serum_order(serum_order)]
|
||||
|
||||
return orders
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"""« 𝚂𝚎𝚛𝚞𝚖𝙾𝚛𝚍𝚎𝚛𝙿𝚕𝚊𝚌𝚎𝚛 [{self.spot_market.symbol}] »"""
|
||||
return f"""« 𝚂𝚎𝚛𝚞𝚖𝙼𝚊𝚛𝚔𝚎𝚝𝙾𝚙𝚎𝚛𝚊𝚝𝚒𝚘𝚗𝚜 [{self.serum_market.symbol}] »"""
|
||||
|
|
|
@ -27,9 +27,10 @@ from .token import Token
|
|||
|
||||
|
||||
class SpotMarket(Market):
|
||||
def __init__(self, base: Token, quote: Token, address: PublicKey):
|
||||
def __init__(self, base: Token, quote: Token, address: PublicKey, group_address: PublicKey):
|
||||
super().__init__(base, quote)
|
||||
self.address: PublicKey = address
|
||||
self.group_address: PublicKey = group_address
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"« 𝚂𝚙𝚘𝚝𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol}: {self.address} »"
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
# # ⚠ 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 itertools
|
||||
import typing
|
||||
|
||||
from decimal import Decimal
|
||||
from pyserum.market import Market
|
||||
from pyserum.market.orderbook import OrderBook as SerumOrderBook
|
||||
from pyserum.market.types import Order as SerumOrder
|
||||
from solana.account import Account as SolanaAccount
|
||||
from solana.publickey import PublicKey
|
||||
from solana.transaction import Transaction
|
||||
|
||||
from .account import Account
|
||||
from .accountinfo import AccountInfo
|
||||
from .context import Context
|
||||
from .group import Group
|
||||
from .instructions import build_compound_spot_place_order_instructions, build_cancel_spot_order_instructions
|
||||
from .marketoperations import MarketOperations
|
||||
from .orders import Order, OrderType, Side
|
||||
from .spotmarket import SpotMarket
|
||||
from .tokenaccount import TokenAccount
|
||||
from .wallet import Wallet
|
||||
|
||||
|
||||
# # 🥭 SpotMarketOperations class
|
||||
#
|
||||
# This class puts trades on the Serum orderbook. It doesn't do anything complicated.
|
||||
#
|
||||
|
||||
class SpotMarketOperations(MarketOperations):
|
||||
def __init__(self, context: Context, wallet: Wallet, group: Group, account: Account, spot_market: SpotMarket, reporter: typing.Callable[[str], None] = None):
|
||||
super().__init__()
|
||||
self.context: Context = context
|
||||
self.wallet: Wallet = wallet
|
||||
self.group: Group = group
|
||||
self.account: Account = account
|
||||
self.spot_market: SpotMarket = spot_market
|
||||
self.market: Market = Market.load(context.client, spot_market.address, context.dex_program_id)
|
||||
self._serum_fee_discount_token_address: typing.Optional[PublicKey] = None
|
||||
self._serum_fee_discount_token_address_loaded: bool = False
|
||||
|
||||
market_index: int = -1
|
||||
for index, spot in enumerate(self.group.spot_markets):
|
||||
if spot is not None and spot.address == self.spot_market.address:
|
||||
market_index = index
|
||||
if market_index == -1:
|
||||
raise Exception(f"Could not find spot market {self.spot_market.address} in group {self.group.address}")
|
||||
|
||||
self.group_market_index: int = market_index
|
||||
|
||||
def report(text):
|
||||
self.logger.info(text)
|
||||
reporter(text)
|
||||
|
||||
def just_log(text):
|
||||
self.logger.info(text)
|
||||
|
||||
if reporter is not None:
|
||||
self.reporter = report
|
||||
else:
|
||||
self.reporter = just_log
|
||||
|
||||
@property
|
||||
def serum_fee_discount_token_address(self) -> typing.Optional[PublicKey]:
|
||||
if self._serum_fee_discount_token_address_loaded:
|
||||
return self._serum_fee_discount_token_address
|
||||
|
||||
# SRM is always the token Serum uses for fee discounts
|
||||
token = self.context.token_lookup.find_by_symbol("SRM")
|
||||
if token is None:
|
||||
self._serum_fee_discount_token_address_loaded = True
|
||||
self._serum_fee_discount_token_address = None
|
||||
return self._serum_fee_discount_token_address
|
||||
|
||||
fee_discount_token_account = TokenAccount.fetch_largest_for_owner_and_token(
|
||||
self.context, self.wallet.address, token)
|
||||
if fee_discount_token_account is not None:
|
||||
self._serum_fee_discount_token_address = fee_discount_token_account.address
|
||||
|
||||
self._serum_fee_discount_token_address_loaded = True
|
||||
return self._serum_fee_discount_token_address
|
||||
|
||||
def cancel_order(self, order: Order) -> 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: typing.Sequence[SolanaAccount] = [self.wallet.account]
|
||||
transaction = Transaction()
|
||||
cancel_instructions = build_cancel_spot_order_instructions(
|
||||
self.context, self.wallet, self.group, self.account, self.market, order, open_orders)
|
||||
transaction.instructions.extend(cancel_instructions)
|
||||
response = self.context.client.send_transaction(transaction, *signers, opts=self.context.transaction_options)
|
||||
return self.context.unwrap_transaction_id_or_raise_exception(response)
|
||||
|
||||
def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order:
|
||||
payer_token = self.spot_market.quote if side == Side.BUY else self.spot_market.base
|
||||
payer_token_account = TokenAccount.fetch_largest_for_owner_and_token(
|
||||
self.context, self.wallet.address, payer_token)
|
||||
if payer_token_account is None:
|
||||
raise Exception(f"Could not find a source token account for token {payer_token}.")
|
||||
|
||||
open_orders = self.account.spot_open_orders[self.group_market_index]
|
||||
client_order_id = self.context.random_client_id()
|
||||
report = f"Placing {order_type} {side} order for size {size} at price {price} on market {self.spot_market.symbol} using client ID {client_order_id}."
|
||||
self.logger.info(report)
|
||||
self.reporter(report)
|
||||
|
||||
signers: typing.Sequence[SolanaAccount] = [self.wallet.account]
|
||||
transaction = Transaction()
|
||||
place_instructions = build_compound_spot_place_order_instructions(
|
||||
self.context, self.wallet, self.group, self.account, self.market, payer_token_account.address,
|
||||
open_orders, order_type, side, price, size, client_order_id, self.serum_fee_discount_token_address)
|
||||
|
||||
transaction.instructions.extend(place_instructions)
|
||||
response = self.context.client.send_transaction(transaction, *signers, opts=self.context.transaction_options)
|
||||
self.context.unwrap_transaction_id_or_raise_exception(response)
|
||||
|
||||
return Order(id=0, side=side, price=price, size=size, client_id=client_order_id, owner=self.account.address)
|
||||
|
||||
def _load_serum_orders(self) -> typing.Sequence[SerumOrder]:
|
||||
[bids_info, asks_info] = AccountInfo.load_multiple(
|
||||
self.context, [self.market.state.bids(), self.market.state.asks()])
|
||||
bids_orderbook = SerumOrderBook.from_bytes(self.market.state, bids_info.data)
|
||||
asks_orderbook = SerumOrderBook.from_bytes(self.market.state, asks_info.data)
|
||||
|
||||
return list(itertools.chain(bids_orderbook.orders(), asks_orderbook.orders()))
|
||||
|
||||
def load_orders(self) -> typing.Sequence[Order]:
|
||||
all_orders = self._load_serum_orders()
|
||||
orders: typing.List[Order] = []
|
||||
for serum_order in all_orders:
|
||||
orders += [Order.from_serum_order(serum_order)]
|
||||
|
||||
return orders
|
||||
|
||||
def load_my_orders(self) -> typing.Sequence[Order]:
|
||||
open_orders_account = self.account.spot_open_orders[self.group_market_index]
|
||||
if not open_orders_account:
|
||||
return []
|
||||
|
||||
all_orders = self._load_serum_orders()
|
||||
serum_orders = [o for o in all_orders if o.open_order_address == open_orders_account]
|
||||
orders: typing.List[Order] = []
|
||||
for serum_order in serum_orders:
|
||||
orders += [Order.from_serum_order(serum_order)]
|
||||
|
||||
return orders
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"""« 𝚂𝚙𝚘𝚝𝙼𝚊𝚛𝚔𝚎𝚝𝙾𝚙𝚎𝚛𝚊𝚝𝚒𝚘𝚗𝚜 [{self.spot_market.symbol}] »"""
|
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
MARKET="BTC-PERP"
|
||||
POSITION_SIZE="0.01"
|
||||
SLEEP_BETWEEN_ORDER_PLACES="60"
|
||||
MARKET=${1:-BTC-PERP}
|
||||
POSITION_SIZE=${2:-0.01}
|
||||
SLEEP_BETWEEN_ORDER_PLACES=${3:-60}
|
||||
let fixed_spread=100
|
||||
|
||||
printf "Running on market %s with position size %f\nPress Control+C to stop...\n" $MARKET $POSITION_SIZE
|
||||
|
|
|
@ -34,8 +34,8 @@ def fake_account_info(address: PublicKey = fake_public_key(), executable: bool =
|
|||
return mango.AccountInfo(address, executable, lamports, owner, rent_epoch, data)
|
||||
|
||||
|
||||
def fake_token() -> mango.Token:
|
||||
return mango.Token("FAKE", "Fake Token", fake_seeded_public_key("fake token"), Decimal(6))
|
||||
def fake_token(symbol: str = "FAKE") -> mango.Token:
|
||||
return mango.Token(symbol, f"Fake Token ({symbol})", fake_seeded_public_key(f"fake token ({symbol})"), Decimal(6))
|
||||
|
||||
|
||||
def fake_token_info() -> mango.TokenInfo:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from .context import mango
|
||||
from .fakes import fake_context, fake_public_key
|
||||
from .fakes import fake_public_key, fake_token
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
|
@ -7,16 +7,15 @@ import datetime
|
|||
|
||||
|
||||
def test_liquidation_event():
|
||||
token_lookup = fake_context().token_lookup
|
||||
balances_before = [
|
||||
mango.TokenValue(token_lookup.find_by_symbol("ETH"), Decimal(1)),
|
||||
mango.TokenValue(token_lookup.find_by_symbol("BTC"), Decimal("0.1")),
|
||||
mango.TokenValue(token_lookup.find_by_symbol("USDT"), Decimal(1000))
|
||||
mango.TokenValue(fake_token("ETH"), Decimal(1)),
|
||||
mango.TokenValue(fake_token("BTC"), Decimal("0.1")),
|
||||
mango.TokenValue(fake_token("USDT"), Decimal(1000))
|
||||
]
|
||||
balances_after = [
|
||||
mango.TokenValue(token_lookup.find_by_symbol("ETH"), Decimal(1)),
|
||||
mango.TokenValue(token_lookup.find_by_symbol("BTC"), Decimal("0.05")),
|
||||
mango.TokenValue(token_lookup.find_by_symbol("USDT"), Decimal(2000))
|
||||
mango.TokenValue(fake_token("ETH"), Decimal(1)),
|
||||
mango.TokenValue(fake_token("BTC"), Decimal("0.05")),
|
||||
mango.TokenValue(fake_token("USDT"), Decimal(2000))
|
||||
]
|
||||
timestamp = datetime.datetime(2021, 5, 17, 12, 20, 56)
|
||||
event = mango.LiquidationEvent(timestamp, "Liquidator", "Group", True, "signature",
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
from .context import mango
|
||||
from .fakes import fake_seeded_public_key
|
||||
|
||||
from decimal import Decimal
|
||||
from solana.publickey import PublicKey
|
||||
|
||||
|
||||
def test_spot_market_constructor():
|
||||
address = PublicKey("11111111111111111111111111111114")
|
||||
base = mango.Token("BASE", "Base Token", PublicKey("11111111111111111111111111111115"), Decimal(7))
|
||||
quote = mango.Token("QUOTE", "Quote Token", PublicKey("11111111111111111111111111111116"), Decimal(9))
|
||||
actual = mango.SpotMarket(base, quote, address)
|
||||
address = fake_seeded_public_key("spot market address")
|
||||
base = mango.Token("BASE", "Base Token", fake_seeded_public_key("base token"), Decimal(7))
|
||||
quote = mango.Token("QUOTE", "Quote Token", fake_seeded_public_key("quote token"), Decimal(9))
|
||||
group_address = fake_seeded_public_key("group address")
|
||||
actual = mango.SpotMarket(base, quote, address, group_address)
|
||||
assert actual is not None
|
||||
assert actual.logger is not None
|
||||
assert actual.address == address
|
||||
assert actual.base == base
|
||||
assert actual.quote == quote
|
||||
assert actual.address == address
|
||||
assert actual.group_address == group_address
|
||||
|
|
Loading…
Reference in New Issue