Added ensure-account and ensure-open-orders commands.
This commit is contained in:
parent
f8ce3d52e0
commit
ec37c14d03
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/env pyston3
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), '..')))
|
||||
import mango # nopep8
|
||||
|
||||
parser = argparse.ArgumentParser(description="Ensure a Mango account exists for the wallet and group.")
|
||||
mango.ContextBuilder.add_command_line_parameters(parser)
|
||||
mango.Wallet.add_command_line_parameters(parser)
|
||||
parser.add_argument("--wait", action="store_true", default=False,
|
||||
help="wait until the transaction is confirmed")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.getLogger().setLevel(args.log_level)
|
||||
logging.warning(mango.WARNING_DISCLAIMER_TEXT)
|
||||
|
||||
context = mango.ContextBuilder.from_command_line_parameters(args)
|
||||
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
|
||||
|
||||
group = mango.Group.load(context)
|
||||
accounts = mango.Account.load_all_for_owner(context, wallet.address, group)
|
||||
|
||||
if len(accounts) > 0:
|
||||
print(f"At least one account already exists for group {group.address} and wallet {wallet.address}")
|
||||
else:
|
||||
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet)
|
||||
init = mango.build_create_account_instructions(context, wallet, group)
|
||||
all_instructions = signers + init
|
||||
transaction_ids = all_instructions.execute(context)
|
||||
|
||||
print("Created account.")
|
||||
if args.wait:
|
||||
print("Waiting on transaction IDs:", transaction_ids)
|
||||
context.client.wait_for_confirmation(transaction_ids)
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env pyston3
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), '..')))
|
||||
import mango # nopep8
|
||||
|
||||
parser = argparse.ArgumentParser(description="Ensure an OpenOrders account exists for the wallet and market.")
|
||||
mango.ContextBuilder.add_command_line_parameters(parser)
|
||||
mango.Wallet.add_command_line_parameters(parser)
|
||||
parser.add_argument("--account-index", type=int, default=0,
|
||||
help="index of the account to use, if more than one available")
|
||||
parser.add_argument("--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)")
|
||||
parser.add_argument("--dry-run", action="store_true", default=False,
|
||||
help="runs as read-only and does not perform any transactions")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.getLogger().setLevel(args.log_level)
|
||||
logging.warning(mango.WARNING_DISCLAIMER_TEXT)
|
||||
|
||||
context = mango.ContextBuilder.from_command_line_parameters(args)
|
||||
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
|
||||
|
||||
group = mango.Group.load(context)
|
||||
accounts = mango.Account.load_all_for_owner(context, wallet.address, group)
|
||||
if len(accounts) == 0:
|
||||
raise Exception(f"No Mango account exists for group {group.address} and wallet {wallet.address}")
|
||||
account = accounts[args.account_index]
|
||||
|
||||
market_symbol = args.market.upper()
|
||||
market = context.market_lookup.find_by_symbol(market_symbol)
|
||||
if market is None:
|
||||
raise Exception(f"Could not find market {market_symbol}")
|
||||
|
||||
loaded_market: mango.Market = mango.ensure_market_loaded(context, market)
|
||||
market_operations = mango.create_market_operations(context, wallet, account, market, args.dry_run)
|
||||
open_orders = market_operations.ensure_openorders()
|
||||
print(f"OpenOrders account for {market.symbol} is {open_orders}")
|
|
@ -20,7 +20,8 @@ def report_accrued(basket_token: mango.AccountBasketBaseToken):
|
|||
|
||||
def load_perp_market(context: mango.Context, group: mango.Group, group_basket_market: mango.GroupBasketMarket):
|
||||
perp_market_details = mango.PerpMarketDetails.load(context, group_basket_market.perp_market_info.address, group)
|
||||
perp_market = mango.PerpMarket(group_basket_market.perp_market_info.address, group_basket_market.base_token_info.token,
|
||||
perp_market = mango.PerpMarket(context.program_id, group_basket_market.perp_market_info.address,
|
||||
group_basket_market.base_token_info.token,
|
||||
group_basket_market.quote_token_info.token, perp_market_details)
|
||||
|
||||
return perp_market
|
||||
|
|
|
@ -16,7 +16,7 @@ parser = argparse.ArgumentParser(description="Shows details of a Merps account."
|
|||
mango.ContextBuilder.add_command_line_parameters(parser)
|
||||
mango.Wallet.add_command_line_parameters(parser)
|
||||
parser.add_argument("--address", type=PublicKey, required=False,
|
||||
help="address of Merps account (defaults to the root address of the wallet)")
|
||||
help="address of account (defaults to the root address of the wallet)")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.getLogger().setLevel(args.log_level)
|
||||
|
|
|
@ -151,11 +151,12 @@ class ContextBuilder:
|
|||
ids_json_market_lookup: MarketLookup = IdsJsonMarketLookup(cluster)
|
||||
all_market_lookup = ids_json_market_lookup
|
||||
if cluster == "mainnet-beta":
|
||||
mainnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(token_filename)
|
||||
mainnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(dex_program_id, token_filename)
|
||||
all_market_lookup = CompoundMarketLookup([ids_json_market_lookup, mainnet_serum_market_lookup])
|
||||
elif cluster == "devnet":
|
||||
devnet_token_filename = token_filename.rsplit('.', 1)[0] + ".devnet.json"
|
||||
devnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(devnet_token_filename)
|
||||
devnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(
|
||||
dex_program_id, devnet_token_filename)
|
||||
all_market_lookup = CompoundMarketLookup([ids_json_market_lookup, devnet_serum_market_lookup])
|
||||
market_lookup: MarketLookup = all_market_lookup
|
||||
|
||||
|
|
|
@ -50,15 +50,15 @@ class IdsJsonMarketLookup(MarketLookup):
|
|||
self.cluster: str = cluster
|
||||
|
||||
@staticmethod
|
||||
def _from_dict(market_type: IdsJsonMarketType, group_address: PublicKey, data: typing.Dict, tokens: typing.Sequence[Token], quote_symbol: str) -> Market:
|
||||
def _from_dict(market_type: IdsJsonMarketType, program_id: PublicKey, group_address: PublicKey, data: typing.Dict, tokens: typing.Sequence[Token], quote_symbol: str) -> Market:
|
||||
base_symbol = data["baseSymbol"]
|
||||
base = Token.find_by_symbol(tokens, base_symbol)
|
||||
quote = Token.find_by_symbol(tokens, quote_symbol)
|
||||
address = PublicKey(data["publicKey"])
|
||||
if market_type == IdsJsonMarketType.PERP:
|
||||
return PerpMarketStub(address, base, quote, group_address)
|
||||
return PerpMarketStub(program_id, address, base, quote, group_address)
|
||||
else:
|
||||
return SpotMarketStub(address, base, quote, group_address)
|
||||
return SpotMarketStub(program_id, address, base, quote, group_address)
|
||||
|
||||
@staticmethod
|
||||
def _load_tokens(data: typing.Dict) -> typing.Sequence[Token]:
|
||||
|
@ -82,30 +82,32 @@ class IdsJsonMarketLookup(MarketLookup):
|
|||
for group in MangoConstants["groups"]:
|
||||
if group["cluster"] == self.cluster:
|
||||
group_address: PublicKey = PublicKey(group["publicKey"])
|
||||
program_id: PublicKey = PublicKey(group["mangoProgramId"])
|
||||
if check_perps:
|
||||
for market_data in group["perpMarkets"]:
|
||||
if market_data["name"].upper() == symbol.upper():
|
||||
tokens = IdsJsonMarketLookup._load_tokens(group["tokens"])
|
||||
return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, group_address, market_data, tokens, group["quoteSymbol"])
|
||||
return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, program_id, group_address, market_data, tokens, group["quoteSymbol"])
|
||||
if check_spots:
|
||||
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 IdsJsonMarketLookup._from_dict(IdsJsonMarketType.SPOT, program_id, 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:
|
||||
group_address: PublicKey = PublicKey(group["publicKey"])
|
||||
program_id: PublicKey = PublicKey(group["mangoProgramId"])
|
||||
for market_data in group["perpMarkets"]:
|
||||
if market_data["key"] == str(address):
|
||||
tokens = IdsJsonMarketLookup._load_tokens(group["tokens"])
|
||||
return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, group_address, market_data, tokens, group["quoteSymbol"])
|
||||
return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, program_id, 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 IdsJsonMarketLookup._from_dict(IdsJsonMarketType.SPOT, program_id, group_address, market_data, tokens, group["quoteSymbol"])
|
||||
return None
|
||||
|
||||
def all_markets(self) -> typing.Sequence[Market]:
|
||||
|
@ -113,15 +115,16 @@ class IdsJsonMarketLookup(MarketLookup):
|
|||
for group in MangoConstants["groups"]:
|
||||
if group["cluster"] == self.cluster:
|
||||
group_address: PublicKey = PublicKey(group["publicKey"])
|
||||
program_id: PublicKey = PublicKey(group["mangoProgramId"])
|
||||
for market_data in group["perpMarkets"]:
|
||||
tokens = IdsJsonMarketLookup._load_tokens(group["tokens"])
|
||||
market = IdsJsonMarketLookup._from_dict(
|
||||
IdsJsonMarketType.PERP, group_address, market_data, tokens, group["quoteSymbol"])
|
||||
IdsJsonMarketType.PERP, program_id, 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"])
|
||||
IdsJsonMarketType.SPOT, program_id, group_address, market_data, tokens, group["quoteSymbol"])
|
||||
markets = [market]
|
||||
|
||||
return markets
|
||||
|
|
|
@ -40,8 +40,9 @@ class InventorySource(enum.Enum):
|
|||
#
|
||||
|
||||
class Market(metaclass=abc.ABCMeta):
|
||||
def __init__(self, address: PublicKey, inventory_source: InventorySource, base: Token, quote: Token):
|
||||
def __init__(self, program_id: PublicKey, address: PublicKey, inventory_source: InventorySource, base: Token, quote: Token):
|
||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||
self.program_id: PublicKey = program_id
|
||||
self.address: PublicKey = address
|
||||
self.inventory_source: InventorySource = inventory_source
|
||||
self.base: Token = base
|
||||
|
|
|
@ -19,7 +19,9 @@ import logging
|
|||
import typing
|
||||
|
||||
from decimal import Decimal
|
||||
from solana.publickey import PublicKey
|
||||
|
||||
from .constants import SYSTEM_PROGRAM_ADDRESS
|
||||
from .orders import Order
|
||||
|
||||
|
||||
|
@ -48,8 +50,6 @@ from .orders import Order
|
|||
# market_operations.place_order(order)
|
||||
# ```
|
||||
#
|
||||
|
||||
|
||||
class MarketOperations(metaclass=abc.ABCMeta):
|
||||
def __init__(self):
|
||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||
|
@ -78,6 +78,14 @@ class MarketOperations(metaclass=abc.ABCMeta):
|
|||
def crank(self, limit: Decimal = Decimal(32)) -> typing.Sequence[str]:
|
||||
raise NotImplementedError("MarketOperations.crank() is not implemented on the base type.")
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_openorders(self) -> PublicKey:
|
||||
raise NotImplementedError("MarketOperations.create_openorders() is not implemented on the base type.")
|
||||
|
||||
@abc.abstractmethod
|
||||
def ensure_openorders(self) -> PublicKey:
|
||||
raise NotImplementedError("MarketOperations.ensure_openorders() is not implemented on the base type.")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self}"
|
||||
|
||||
|
@ -87,7 +95,6 @@ class MarketOperations(metaclass=abc.ABCMeta):
|
|||
# A null, no-op, dry-run trade executor that can be plugged in anywhere a `MarketOperations`
|
||||
# is expected, but which will not actually trade.
|
||||
#
|
||||
|
||||
class NullMarketOperations(MarketOperations):
|
||||
def __init__(self, market_name: str):
|
||||
super().__init__()
|
||||
|
@ -113,5 +120,11 @@ class NullMarketOperations(MarketOperations):
|
|||
def crank(self, limit: Decimal = Decimal(32)) -> typing.Sequence[str]:
|
||||
return []
|
||||
|
||||
def create_openorders(self) -> PublicKey:
|
||||
return SYSTEM_PROGRAM_ADDRESS
|
||||
|
||||
def ensure_openorders(self) -> PublicKey:
|
||||
return SYSTEM_PROGRAM_ADDRESS
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"""« 𝙽𝚞𝚕𝚕𝙾𝚛𝚍𝚎𝚛𝙿𝚕𝚊𝚌𝚎𝚛 [{self.market_name}] »"""
|
||||
|
|
|
@ -73,7 +73,8 @@ class SerumOracle(Oracle):
|
|||
context.client.commitment,
|
||||
context.client.skip_preflight,
|
||||
context.client.instruction_reporter)
|
||||
mainnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(SplTokenLookup.DefaultDataFilepath)
|
||||
mainnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(
|
||||
context.dex_program_id, SplTokenLookup.DefaultDataFilepath)
|
||||
adjusted_market = self.market
|
||||
mainnet_adjusted_market: typing.Optional[Market] = mainnet_serum_market_lookup.find_by_symbol(
|
||||
self.market.symbol)
|
||||
|
@ -110,7 +111,7 @@ class SerumOracleProvider(OracleProvider):
|
|||
def oracle_for_market(self, context: Context, market: Market) -> typing.Optional[Oracle]:
|
||||
loaded_market: Market = ensure_market_loaded(context, market)
|
||||
if isinstance(loaded_market, SpotMarket):
|
||||
serum_market = SerumMarket(loaded_market.address, loaded_market.base,
|
||||
serum_market = SerumMarket(context.dex_program_id, loaded_market.address, loaded_market.base,
|
||||
loaded_market.quote, loaded_market.underlying_serum_market)
|
||||
return SerumOracle(serum_market)
|
||||
elif isinstance(loaded_market, SerumMarket):
|
||||
|
|
|
@ -33,10 +33,9 @@ from .token import Token
|
|||
#
|
||||
# This class encapsulates our knowledge of a Mango perps market.
|
||||
#
|
||||
|
||||
class PerpMarket(Market):
|
||||
def __init__(self, address: PublicKey, base: Token, quote: Token, underlying_perp_market: PerpMarketDetails):
|
||||
super().__init__(address, InventorySource.ACCOUNT, base, quote)
|
||||
def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, underlying_perp_market: PerpMarketDetails):
|
||||
super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote)
|
||||
self.underlying_perp_market: PerpMarketDetails = underlying_perp_market
|
||||
self.lot_size_converter: LotSizeConverter = LotSizeConverter(
|
||||
base, underlying_perp_market.base_lot_size, quote, underlying_perp_market.quote_lot_size)
|
||||
|
@ -82,7 +81,7 @@ class PerpMarket(Market):
|
|||
|
||||
def __str__(self) -> str:
|
||||
underlying: str = f"{self.underlying_perp_market}".replace("\n", "\n ")
|
||||
return f"""« 𝙿𝚎𝚛𝚙𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} [{self.address}]
|
||||
return f"""« 𝙿𝚎𝚛𝚙𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} {self.address} [{self.program_id}]
|
||||
{underlying}
|
||||
»"""
|
||||
|
||||
|
@ -91,20 +90,19 @@ class PerpMarket(Market):
|
|||
#
|
||||
# This class holds information to load a `PerpMarket` object but doesn't automatically load it.
|
||||
#
|
||||
|
||||
class PerpMarketStub(Market):
|
||||
def __init__(self, address: PublicKey, base: Token, quote: Token, group_address: PublicKey):
|
||||
super().__init__(address, InventorySource.ACCOUNT, base, quote)
|
||||
def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, group_address: PublicKey):
|
||||
super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote)
|
||||
self.group_address: PublicKey = group_address
|
||||
|
||||
def load(self, context: Context, group: typing.Optional[Group] = None) -> PerpMarket:
|
||||
actual_group: Group = group or Group.load(context, self.group_address)
|
||||
underlying_perp_market: PerpMarketDetails = PerpMarketDetails.load(context, self.address, actual_group)
|
||||
return PerpMarket(self.address, self.base, self.quote, underlying_perp_market)
|
||||
return PerpMarket(self.program_id, self.address, self.base, self.quote, underlying_perp_market)
|
||||
|
||||
@property
|
||||
def symbol(self) -> str:
|
||||
return f"{self.base.symbol}-PERP"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"« 𝙿𝚎𝚛𝚙𝙼𝚊𝚛𝚔𝚎𝚝𝚂𝚝𝚞𝚋 {self.symbol} [{self.address}] »"
|
||||
return f"« 𝙿𝚎𝚛𝚙𝙼𝚊𝚛𝚔𝚎𝚝𝚂𝚝𝚞𝚋 {self.symbol} {self.address} [{self.program_id}] »"
|
||||
|
|
|
@ -17,9 +17,11 @@
|
|||
import typing
|
||||
|
||||
from decimal import Decimal
|
||||
from solana.publickey import PublicKey
|
||||
|
||||
from .account import Account
|
||||
from .combinableinstructions import CombinableInstructions
|
||||
from .constants import SYSTEM_PROGRAM_ADDRESS
|
||||
from .context import Context
|
||||
from .marketoperations import MarketOperations
|
||||
from .orders import Order
|
||||
|
@ -32,8 +34,6 @@ from .wallet import Wallet
|
|||
#
|
||||
# This file deals with placing orders for Perps.
|
||||
#
|
||||
|
||||
|
||||
class PerpMarketOperations(MarketOperations):
|
||||
def __init__(self, market_name: str, context: Context, wallet: Wallet,
|
||||
market_instruction_builder: PerpMarketInstructionBuilder,
|
||||
|
@ -80,6 +80,12 @@ class PerpMarketOperations(MarketOperations):
|
|||
crank = self.market_instruction_builder.build_crank_instructions(accounts_to_crank, limit)
|
||||
return (signers + crank).execute(self.context)
|
||||
|
||||
def create_openorders(self) -> PublicKey:
|
||||
return SYSTEM_PROGRAM_ADDRESS
|
||||
|
||||
def ensure_openorders(self) -> PublicKey:
|
||||
return SYSTEM_PROGRAM_ADDRESS
|
||||
|
||||
def load_orders(self) -> typing.Sequence[Order]:
|
||||
return self.perp_market.orders(self.context)
|
||||
|
||||
|
|
|
@ -32,11 +32,9 @@ from .token import Token
|
|||
#
|
||||
# This class encapsulates our knowledge of a Serum spot market.
|
||||
#
|
||||
|
||||
|
||||
class SerumMarket(Market):
|
||||
def __init__(self, address: PublicKey, base: Token, quote: Token, underlying_serum_market: PySerumMarket):
|
||||
super().__init__(address, InventorySource.SPL_TOKENS, base, quote)
|
||||
def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, underlying_serum_market: PySerumMarket):
|
||||
super().__init__(program_id, address, InventorySource.SPL_TOKENS, base, quote)
|
||||
self.underlying_serum_market: PySerumMarket = underlying_serum_market
|
||||
|
||||
def unprocessed_events(self, context: Context) -> typing.Sequence[SerumEvent]:
|
||||
|
@ -53,7 +51,7 @@ class SerumMarket(Market):
|
|||
return list(map(Order.from_serum_order, itertools.chain(bids_orderbook.orders(), asks_orderbook.orders())))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"""« 𝚂𝚎𝚛𝚞𝚖𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} [{self.address}]
|
||||
return f"""« 𝚂𝚎𝚛𝚞𝚖𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} {self.address} [{self.program_id}]
|
||||
Event Queue: {self.underlying_serum_market.state.event_queue()}
|
||||
Request Queue: {self.underlying_serum_market.state.request_queue()}
|
||||
Bids: {self.underlying_serum_market.state.bids()}
|
||||
|
@ -67,16 +65,14 @@ class SerumMarket(Market):
|
|||
#
|
||||
# This class holds information to load a `SerumMarket` object but doesn't automatically load it.
|
||||
#
|
||||
|
||||
|
||||
class SerumMarketStub(Market):
|
||||
def __init__(self, address: PublicKey, base: Token, quote: Token):
|
||||
super().__init__(address, InventorySource.SPL_TOKENS, base, quote)
|
||||
def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token):
|
||||
super().__init__(program_id, address, InventorySource.SPL_TOKENS, base, quote)
|
||||
|
||||
def load(self, context: Context) -> SerumMarket:
|
||||
underlying_serum_market: PySerumMarket = PySerumMarket.load(
|
||||
context.client.compatible_client, self.address, context.dex_program_id)
|
||||
return SerumMarket(self.address, self.base, self.quote, underlying_serum_market)
|
||||
return SerumMarket(self.program_id, self.address, self.base, self.quote, underlying_serum_market)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"« 𝚂𝚎𝚛𝚞𝚖𝙼𝚊𝚛𝚔𝚎𝚝𝚂𝚝𝚞𝚋 {self.symbol} [{self.address}] »"
|
||||
return f"« 𝚂𝚎𝚛𝚞𝚖𝙼𝚊𝚛𝚔𝚎𝚝𝚂𝚝𝚞𝚋 {self.symbol} {self.address} [{self.program_id}] »"
|
||||
|
|
|
@ -41,7 +41,6 @@ from .wallet import Wallet
|
|||
# existing data, requiring no fetches from Solana or other sources. All necessary data should all be loaded
|
||||
# on initial setup in the `load()` method.
|
||||
#
|
||||
|
||||
class SerumMarketInstructionBuilder(MarketInstructionBuilder):
|
||||
def __init__(self, context: Context, wallet: Wallet, serum_market: SerumMarket, raw_market: Market, base_token_account: TokenAccount, quote_token_account: TokenAccount, open_orders_address: typing.Optional[PublicKey], fee_discount_token_address: typing.Optional[PublicKey]):
|
||||
super().__init__()
|
||||
|
@ -96,10 +95,7 @@ class SerumMarketInstructionBuilder(MarketInstructionBuilder):
|
|||
def build_place_order_instructions(self, order: Order) -> CombinableInstructions:
|
||||
ensure_open_orders = CombinableInstructions.empty()
|
||||
if self.open_orders_address is None:
|
||||
ensure_open_orders = build_create_serum_open_orders_instructions(
|
||||
self.context, self.wallet, self.raw_market)
|
||||
|
||||
self.open_orders_address = ensure_open_orders.signers[0].public_key()
|
||||
ensure_open_orders = self.build_create_openorders_instructions()
|
||||
|
||||
serum_order_type: pyserum.enums.OrderType = order.order_type.to_serum()
|
||||
serum_side: pyserum.enums.Side = order.side.to_serum()
|
||||
|
@ -134,5 +130,10 @@ class SerumMarketInstructionBuilder(MarketInstructionBuilder):
|
|||
|
||||
return build_serum_consume_events_instructions(self.context, self.serum_market.address, self.raw_market.state.event_queue(), limited_open_orders_addresses, int(limit))
|
||||
|
||||
def build_create_openorders_instructions(self) -> CombinableInstructions:
|
||||
create_open_orders = build_create_serum_open_orders_instructions(self.context, self.wallet, self.raw_market)
|
||||
self.open_orders_address = create_open_orders.signers[0].public_key()
|
||||
return create_open_orders
|
||||
|
||||
def __str__(self) -> str:
|
||||
return """« 𝚂𝚎𝚛𝚞𝚖𝙼𝚊𝚛𝚔𝚎𝚝𝙸𝚗𝚜𝚝𝚛𝚞𝚌𝚝𝚒𝚘𝚗𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »"""
|
||||
|
|
|
@ -49,18 +49,17 @@ from .token import Token
|
|||
# the list, check if the item has the optional `extensions` attribute, and in there see if
|
||||
# there is a name-value pair for the particular market we're interested in. Also, the
|
||||
# current file only lists USDC and USDT markets, so that's all we can support this way.
|
||||
|
||||
|
||||
class SerumMarketLookup(MarketLookup):
|
||||
def __init__(self, token_data: typing.Dict) -> None:
|
||||
def __init__(self, program_id: PublicKey, token_data: typing.Dict) -> None:
|
||||
super().__init__()
|
||||
self.program_id: PublicKey = program_id
|
||||
self.token_data: typing.Dict = token_data
|
||||
|
||||
@staticmethod
|
||||
def load(token_data_filename: str) -> "SerumMarketLookup":
|
||||
def load(program_id: PublicKey, token_data_filename: str) -> "SerumMarketLookup":
|
||||
with open(token_data_filename) as json_file:
|
||||
token_data = json.load(json_file)
|
||||
return SerumMarketLookup(token_data)
|
||||
return SerumMarketLookup(program_id, token_data)
|
||||
|
||||
@staticmethod
|
||||
def _find_data_by_symbol(symbol: str, token_data: typing.Dict) -> typing.Optional[typing.Dict]:
|
||||
|
@ -122,7 +121,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 SerumMarketStub(market_address, base, quote)
|
||||
return SerumMarketStub(self.program_id, market_address, base, quote)
|
||||
|
||||
def find_by_address(self, address: PublicKey) -> typing.Optional[Market]:
|
||||
address_string: str = str(address)
|
||||
|
@ -139,7 +138,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 SerumMarketStub(market_address, base, quote)
|
||||
return SerumMarketStub(self.program_id, market_address, base, quote)
|
||||
if "serumV3Usdt" in token_data["extensions"]:
|
||||
if token_data["extensions"]["serumV3Usdt"] == address_string:
|
||||
market_address_string = token_data["extensions"]["serumV3Usdt"]
|
||||
|
@ -151,7 +150,7 @@ 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 SerumMarketStub(market_address, base, quote)
|
||||
return SerumMarketStub(self.program_id, market_address, base, quote)
|
||||
return None
|
||||
|
||||
def all_markets(self) -> typing.Sequence[Market]:
|
||||
|
@ -166,12 +165,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 += [SerumMarketStub(market_address, base, usdc)]
|
||||
all_markets += [SerumMarketStub(self.program_id, market_address, base, usdc)]
|
||||
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 += [SerumMarketStub(market_address, base, usdt)]
|
||||
all_markets += [SerumMarketStub(self.program_id, market_address, base, usdt)]
|
||||
|
||||
return all_markets
|
||||
|
|
|
@ -31,9 +31,8 @@ from .wallet import Wallet
|
|||
|
||||
# # 🥭 SerumMarketOperations class
|
||||
#
|
||||
# This class puts trades on the Serum orderbook. It doesn't do anything complicated.
|
||||
# This class performs standard operations on the Serum orderbook.
|
||||
#
|
||||
|
||||
class SerumMarketOperations(MarketOperations):
|
||||
def __init__(self, context: Context, wallet: Wallet, serum_market: SerumMarket, market_instruction_builder: SerumMarketInstructionBuilder):
|
||||
super().__init__()
|
||||
|
@ -78,6 +77,19 @@ class SerumMarketOperations(MarketOperations):
|
|||
crank = self._build_crank(limit)
|
||||
return (signers + crank).execute(self.context)
|
||||
|
||||
def create_openorders(self) -> PublicKey:
|
||||
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
|
||||
create_open_orders = self.market_instruction_builder.build_create_openorders_instructions()
|
||||
open_orders_address = create_open_orders.signers[0].public_key()
|
||||
(signers + create_open_orders).execute(self.context)
|
||||
|
||||
return open_orders_address
|
||||
|
||||
def ensure_openorders(self) -> PublicKey:
|
||||
if self.market_instruction_builder.open_orders_address is not None:
|
||||
return self.market_instruction_builder.open_orders_address
|
||||
return self.create_openorders()
|
||||
|
||||
def load_orders(self) -> typing.Sequence[Order]:
|
||||
return self.serum_market.orders(self.context)
|
||||
|
||||
|
|
|
@ -33,11 +33,9 @@ from .token import Token
|
|||
#
|
||||
# This class encapsulates our knowledge of a Serum spot market.
|
||||
#
|
||||
|
||||
|
||||
class SpotMarket(Market):
|
||||
def __init__(self, address: PublicKey, base: Token, quote: Token, group: Group, underlying_serum_market: PySerumMarket):
|
||||
super().__init__(address, InventorySource.ACCOUNT, base, quote)
|
||||
def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, group: Group, underlying_serum_market: PySerumMarket):
|
||||
super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote)
|
||||
self.group: Group = group
|
||||
self.underlying_serum_market: PySerumMarket = underlying_serum_market
|
||||
|
||||
|
@ -55,7 +53,7 @@ class SpotMarket(Market):
|
|||
return list(map(Order.from_serum_order, itertools.chain(bids_orderbook.orders(), asks_orderbook.orders())))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"""« 𝚂𝚙𝚘𝚝𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} [{self.address}]
|
||||
return f"""« 𝚂𝚙𝚘𝚝𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} {self.address} [{self.program_id}]
|
||||
Event Queue: {self.underlying_serum_market.state.event_queue()}
|
||||
Request Queue: {self.underlying_serum_market.state.request_queue()}
|
||||
Bids: {self.underlying_serum_market.state.bids()}
|
||||
|
@ -72,15 +70,15 @@ class SpotMarket(Market):
|
|||
|
||||
|
||||
class SpotMarketStub(Market):
|
||||
def __init__(self, address: PublicKey, base: Token, quote: Token, group_address: PublicKey):
|
||||
super().__init__(address, InventorySource.ACCOUNT, base, quote)
|
||||
def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, group_address: PublicKey):
|
||||
super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote)
|
||||
self.group_address: PublicKey = group_address
|
||||
|
||||
def load(self, context: Context, group: typing.Optional[Group]) -> SpotMarket:
|
||||
actual_group: Group = group or Group.load(context, self.group_address)
|
||||
underlying_serum_market: PySerumMarket = PySerumMarket.load(
|
||||
context.client.compatible_client, self.address, context.dex_program_id)
|
||||
return SpotMarket(self.address, self.base, self.quote, actual_group, underlying_serum_market)
|
||||
return SpotMarket(self.program_id, self.address, self.base, self.quote, actual_group, underlying_serum_market)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"« 𝚂𝚙𝚘𝚝𝙼𝚊𝚛𝚔𝚎𝚝𝚂𝚝𝚞𝚋 {self.symbol} [{self.address}] »"
|
||||
return f"« 𝚂𝚙𝚘𝚝𝙼𝚊𝚛𝚔𝚎𝚝𝚂𝚝𝚞𝚋 {self.symbol} {self.address} [{self.program_id}] »"
|
||||
|
|
|
@ -35,7 +35,6 @@ from .wallet import Wallet
|
|||
#
|
||||
# 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, market_instruction_builder: SpotMarketInstructionBuilder):
|
||||
super().__init__()
|
||||
|
@ -86,6 +85,24 @@ class SpotMarketOperations(MarketOperations):
|
|||
crank = self._build_crank(limit, add_self=False)
|
||||
return (signers + crank).execute(self.context)
|
||||
|
||||
def create_openorders(self) -> PublicKey:
|
||||
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
|
||||
create_open_orders: CombinableInstructions = self.market_instruction_builder.build_create_openorders_instructions()
|
||||
open_orders_address: PublicKey = create_open_orders.signers[0].public_key()
|
||||
(signers + create_open_orders).execute(self.context)
|
||||
|
||||
# This line is a little nasty. Now that we know we have an OpenOrders account at this address, update
|
||||
# the Account so that future uses (like later in this method) have access to it in the right place.
|
||||
self.account.update_spot_open_orders_for_market(self.market_index, open_orders_address)
|
||||
|
||||
return open_orders_address
|
||||
|
||||
def ensure_openorders(self) -> PublicKey:
|
||||
existing: typing.Optional[PublicKey] = self.account.spot_open_orders[self.market_index]
|
||||
if existing is not None:
|
||||
return existing
|
||||
return self.create_openorders()
|
||||
|
||||
def load_orders(self) -> typing.Sequence[Order]:
|
||||
return self.spot_market.orders(self.context)
|
||||
|
||||
|
@ -96,14 +113,6 @@ class SpotMarketOperations(MarketOperations):
|
|||
all_orders = self.spot_market.orders(self.context)
|
||||
return list([o for o in all_orders if o.owner == self.open_orders_address])
|
||||
|
||||
def create_openorders_for_market(self) -> PublicKey:
|
||||
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
|
||||
create_open_orders = self.market_instruction_builder.build_create_openorders_instructions()
|
||||
open_orders_address = create_open_orders.signers[0].public_key()
|
||||
(signers + create_open_orders).execute(self.context)
|
||||
|
||||
return open_orders_address
|
||||
|
||||
def _build_crank(self, limit: Decimal = Decimal(32), add_self: bool = False) -> CombinableInstructions:
|
||||
open_orders_to_crank: typing.List[PublicKey] = []
|
||||
for event in self.spot_market.unprocessed_events(self.context):
|
||||
|
|
|
@ -70,13 +70,9 @@ def build_spot_open_orders_watcher(context: Context, manager: WebSocketSubscript
|
|||
context, wallet, spot_market.group, account, spot_market)
|
||||
market_operations: SpotMarketOperations = SpotMarketOperations(
|
||||
context, wallet, spot_market.group, account, spot_market, spot_market_instruction_builder)
|
||||
open_orders_address = market_operations.create_openorders_for_market()
|
||||
open_orders_address = market_operations.create_openorders()
|
||||
logging.info(f"Created {spot_market.symbol} OpenOrders at: {open_orders_address}")
|
||||
|
||||
# This line is a little nasty. Now that we know we have an OpenOrders account at this address, update
|
||||
# the Account so that future uses (like later in this method) have access to it in the right place.
|
||||
account.update_spot_open_orders_for_market(market_index, open_orders_address)
|
||||
|
||||
spot_open_orders_subscription = WebSocketAccountSubscription[OpenOrders](
|
||||
context, open_orders_address, lambda account_info: OpenOrders.parse(account_info, spot_market.base.decimals, spot_market.quote.decimals))
|
||||
manager.add(spot_open_orders_subscription)
|
||||
|
|
|
@ -82,7 +82,7 @@ def fake_market() -> market.Market:
|
|||
|
||||
|
||||
def fake_spot_market_stub() -> mango.SpotMarketStub:
|
||||
return mango.SpotMarketStub(fake_seeded_public_key("spot market"), fake_token("BASE"), fake_token("QUOTE"), fake_seeded_public_key("group address"))
|
||||
return mango.SpotMarketStub(fake_seeded_public_key("program ID"), fake_seeded_public_key("spot market"), fake_token("BASE"), fake_token("QUOTE"), fake_seeded_public_key("group address"))
|
||||
|
||||
|
||||
def fake_token_account() -> mango.TokenAccount:
|
||||
|
|
|
@ -2,6 +2,8 @@ from .context import mango
|
|||
|
||||
from solana.publickey import PublicKey
|
||||
|
||||
from .fakes import fake_seeded_public_key
|
||||
|
||||
|
||||
def test_serum_market_lookup():
|
||||
data = {
|
||||
|
@ -104,7 +106,7 @@ def test_serum_market_lookup():
|
|||
}
|
||||
]
|
||||
}
|
||||
actual = mango.SerumMarketLookup(data)
|
||||
actual = mango.SerumMarketLookup(fake_seeded_public_key("program ID"), data)
|
||||
assert actual is not None
|
||||
assert actual.logger is not None
|
||||
assert actual.find_by_symbol("ETH/USDT") is not None
|
||||
|
@ -114,7 +116,8 @@ def test_serum_market_lookup():
|
|||
|
||||
|
||||
def test_serum_market_lookups_with_full_data():
|
||||
market_lookup = mango.SerumMarketLookup.load(mango.SplTokenLookup.DefaultDataFilepath)
|
||||
market_lookup = mango.SerumMarketLookup.load(fake_seeded_public_key(
|
||||
"program ID"), mango.SplTokenLookup.DefaultDataFilepath)
|
||||
eth_usdt = market_lookup.find_by_symbol("ETH/USDT")
|
||||
assert eth_usdt.base.symbol == "ETH"
|
||||
assert eth_usdt.quote.symbol == "USDT"
|
||||
|
|
|
@ -5,14 +5,16 @@ from decimal import Decimal
|
|||
|
||||
|
||||
def test_spot_market_stub_constructor():
|
||||
program_id = fake_seeded_public_key("program ID")
|
||||
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.SpotMarketStub(address, base, quote, group_address)
|
||||
actual = mango.SpotMarketStub(program_id, address, base, quote, group_address)
|
||||
assert actual is not None
|
||||
assert actual.logger is not None
|
||||
assert actual.base == base
|
||||
assert actual.quote == quote
|
||||
assert actual.address == address
|
||||
assert actual.program_id == program_id
|
||||
assert actual.group_address == group_address
|
||||
|
|
Loading…
Reference in New Issue