Initial work on placing/cancelling spot market orders.

This commit is contained in:
Geoff Taylor 2021-06-30 14:08:37 +01:00
parent 912b7f574a
commit 3e4ba1052c
20 changed files with 622 additions and 78 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

38
mango/perpsmarket.py Normal file
View File

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

38
mango/serummarket.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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