Compare commits

...

3 Commits

30 changed files with 329 additions and 129 deletions

View File

@ -53,7 +53,9 @@ with mango.ContextBuilder.from_command_line_parameters(args) as context:
)
orders = market_operations.load_my_orders(include_expired=True)
if len(orders) == 0:
mango.output(f"No open orders on {market_operations.market.symbol}")
mango.output(
f"No open orders on {market_operations.market.fully_qualified_symbol}"
)
else:
if mango.PerpMarket.isa(market_operations.market):
cancel_all = mango.PerpMarketOperations.ensure(

View File

@ -181,10 +181,12 @@ def cleanup(
dry_run: bool,
) -> None:
market_operations: mango.MarketOperations = mango.operations(
context, wallet, account, market.symbol, dry_run
context, wallet, account, market.fully_qualified_symbol, dry_run
)
market_instruction_builder: mango.MarketInstructionBuilder = (
mango.instruction_builder(context, wallet, account, market.symbol, dry_run)
mango.instruction_builder(
context, wallet, account, market.fully_qualified_symbol, dry_run
)
)
cancels: mango.CombinableInstructions = mango.CombinableInstructions.empty()
orders = market_operations.load_my_orders(include_expired=True)
@ -221,7 +223,7 @@ with mango.ContextBuilder.from_command_line_parameters(args) as context:
# The market index is also the index of the base token in the group's token list.
if market.quote != group.shared_quote_token:
raise Exception(
f"Group {group.name} uses shared quote token {group.shared_quote_token.symbol}/{group.shared_quote_token.mint}, but market {market.symbol} uses quote token {market.quote.symbol}/{market.quote.mint}."
f"Group {group.name} uses shared quote token {group.shared_quote_token.symbol}/{group.shared_quote_token.mint}, but market {market.fully_qualified_symbol} uses quote token {market.quote.symbol}/{market.quote.mint}."
)
cleanup(context, wallet, account, market, args.dry_run)
@ -230,7 +232,7 @@ with mango.ContextBuilder.from_command_line_parameters(args) as context:
if args.hedging_market is not None:
if not mango.PerpMarket.isa(market):
raise Exception(
f"Cannot hedge - market {market.symbol} is not a perp market."
f"Cannot hedge - market {market.fully_qualified_symbol} is not a perp market."
)
underlying_market = mango.PerpMarket.ensure(market)
@ -243,7 +245,7 @@ with mango.ContextBuilder.from_command_line_parameters(args) as context:
f"MarketOperations for {args.hedging_market} is not a SpotMarketOperations."
)
logging.info(f"Hedging on {hedging_ops.market.symbol}")
logging.info(f"Hedging on {hedging_ops.market.fully_qualified_symbol}")
target_balance: typing.Optional[
mango.TargetBalance
@ -287,7 +289,9 @@ with mango.ContextBuilder.from_command_line_parameters(args) as context:
logging.info(f"Desired orders chain: {desired_orders_chain}")
market_instruction_builder: mango.MarketInstructionBuilder = (
mango.instruction_builder(context, wallet, account, market.symbol, args.dry_run)
mango.instruction_builder(
context, wallet, account, market.fully_qualified_symbol, args.dry_run
)
)
market_maker = mango.marketmaking.MarketMaker(
@ -310,7 +314,7 @@ with mango.ContextBuilder.from_command_line_parameters(args) as context:
oracle = oracle_provider.oracle_for_market(context, oracle_market)
if oracle is None:
raise Exception(
f"Could not find oracle for market {oracle_market.symbol} from provider {args.oracle_provider}."
f"Could not find oracle for market {oracle_market.fully_qualified_symbol} from provider {args.oracle_provider}."
)
model_state_builder: mango.marketmaking.ModelStateBuilder = (

View File

@ -5,6 +5,7 @@ import logging
import os
import os.path
import sys
import typing
from decimal import Decimal
from solana.publickey import PublicKey
@ -23,7 +24,13 @@ parser.add_argument(
help="Destination address for the SPL token - can be either the actual token address or the address of the owner of the token address",
)
parser.add_argument(
"--quantity", type=Decimal, required=True, help="quantity of token to send"
"--quantity", type=Decimal, required=False, help="quantity of token to send"
)
parser.add_argument(
"--wallet-target",
type=Decimal,
required=False,
help="wallet balance of token to target by sending remainder",
)
parser.add_argument(
"--dry-run",
@ -39,16 +46,69 @@ parser.add_argument(
)
args: argparse.Namespace = mango.parse_args(parser)
def __quantity_from_wallet_target(
context: mango.Context,
signers: mango.CombinableInstructions,
token: mango.Token,
balance: Decimal,
to_keep: typing.Optional[Decimal],
) -> typing.Optional[Decimal]:
if to_keep is None:
return None
# To accurately calculate the cost we need to build the transaction we're going to send.
#
# But we don't know the quantity we're sending until we know the cost.
#
# So here we but together a fake transaction for sending zero SOL from
# SYSTEM_PROGRAM_ADDRESS to SYSTEM_PROGRAM_ADDRESS, and use that to calculate the cost so
# we can take the cost into account when figuring out how much to send.
#
# (Using SYSTEM_PROGRAM_ADDRESS and zero shouldn't affect the calculations but that may
# change when getFeeForMessage() is used.)
params = TransferParams(
from_pubkey=mango.SYSTEM_PROGRAM_ADDRESS,
to_pubkey=mango.SYSTEM_PROGRAM_ADDRESS,
lamports=0,
)
fake_instruction = mango.CombinableInstructions.from_instruction(transfer(params))
cost = (signers + fake_instruction).cost_to_execute(context)
to_send = balance - to_keep - cost
if to_send < 0:
raise Exception(
f"Cannot achieve wallet balance target of {to_keep:,.8f} {token.symbol} by depositing - wallet only has balance of {balance:,.8f} {token.symbol}"
)
return token.round(to_send, mango.RoundDirection.DOWN)
with mango.ContextBuilder.from_command_line_parameters(args) as context:
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
logging.info(f"Wallet address: {wallet.address}")
token = mango.SolToken
sol_balance = context.client.get_balance(wallet.address)
mango.output(f"Balance: {sol_balance} SOL")
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(
wallet
)
quantity: typing.Optional[Decimal] = args.quantity or __quantity_from_wallet_target(
context, signers, token, sol_balance, args.wallet_target
)
if quantity is None:
raise Exception(
"Neither --quantity nor --wallet-target were specified - must specify one (and only one) of those parameters"
)
if quantity < 0:
raise Exception(f"Cannot send negative quantity {quantity:,.8f} {token.symbol}")
# "A lamport has a value of 0.000000001 SOL." from https://docs.solana.com/introduction
lamports = int(args.quantity * mango.SOL_DECIMAL_DIVISOR)
lamports = int(quantity * mango.SOL_DECIMAL_DIVISOR)
source = wallet.address
destination = args.address

View File

@ -25,7 +25,13 @@ parser.add_argument(
help="Destination address for the SPL token - can be either the actual token address or the address of the owner of the token address",
)
parser.add_argument(
"--quantity", type=Decimal, required=True, help="quantity of token to send"
"--quantity", type=Decimal, required=False, help="quantity of token to send"
)
parser.add_argument(
"--wallet-target",
type=Decimal,
required=False,
help="wallet balance of token to target by sending remainder",
)
parser.add_argument(
"--wait",
@ -41,6 +47,29 @@ parser.add_argument(
)
args: argparse.Namespace = mango.parse_args(parser)
def __quantity_from_wallet_target(
context: mango.Context,
wallet: mango.Wallet,
token: mango.Token,
to_keep: typing.Optional[Decimal],
) -> typing.Optional[Decimal]:
if to_keep is None:
return None
token_accounts: typing.Sequence[
mango.TokenAccount
] = mango.TokenAccount.fetch_all_for_owner_and_token(context, wallet.address, token)
total = sum(acc.value.value for acc in token_accounts)
to_deposit = total - to_keep
if to_deposit < 0:
raise Exception(
f"Cannot achieve wallet balance target of {to_keep:,.8f} {token.symbol} by sending - wallet only has balance of {total:,.8f} {token.symbol}"
)
return token.round(to_deposit, mango.RoundDirection.DOWN)
with mango.ContextBuilder.from_command_line_parameters(args) as context:
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
token: mango.Token = mango.token(context, args.symbol)
@ -86,18 +115,27 @@ with mango.ContextBuilder.from_command_line_parameters(args) as context:
f"Account {args.address} is neither a root wallet account nor an SPL token account."
)
mango.output("Balance:", source.value)
quantity: typing.Optional[Decimal] = args.quantity or __quantity_from_wallet_target(
context, wallet, token, args.wallet_target
)
if quantity is None:
raise Exception(
"Neither --quantity nor --wallet-target were specified - must specify one (and only one) of those parameters"
)
if quantity < 0:
raise Exception(f"Cannot send negative quantity {quantity:,.8f} {token.symbol}")
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(
wallet
)
transfer = mango.build_spl_transfer_tokens_instructions(
context, wallet, token, source.address, destination, args.quantity
context, wallet, token, source.address, destination, quantity
)
mango.output(
"Balance:",
source,
)
amount = token.shift_to_native(args.quantity)
amount = token.shift_to_native(quantity)
text_amount = f"{amount} {token.name} (@ {token.decimals} decimal places)"
creating_marker = "" if create_ata.is_empty else " *CREATING*"
mango.output(f"Sending {text_amount}")

View File

@ -50,7 +50,7 @@ with mango.ContextBuilder.from_command_line_parameters(
oracle = oracle_provider.oracle_for_market(context, market)
if oracle is None:
raise Exception(
f"Could not find oracle for market {market.symbol} from provider {args.oracle_provider}."
f"Could not find oracle for market {market.fully_qualified_symbol} from provider {args.oracle_provider}."
)
health_check = mango.HealthCheck()

View File

@ -38,7 +38,7 @@ with mango.ContextBuilder.from_command_line_parameters(args) as context:
oracle = oracle_provider.oracle_for_market(context, market)
if oracle is None:
mango.output(
f"Could not find oracle for market {market.symbol} from provider {args.provider}."
f"Could not find oracle for market {market.fully_qualified_symbol} from provider {args.provider}."
)
else:
if not args.stream:

View File

@ -41,7 +41,7 @@ with mango.ContextBuilder.from_command_line_parameters(args) as context:
market.quote,
)
mango.output(
f"Found {len(all_open_orders_for_market)} Serum OpenOrders account(s) for market {market.symbol}."
f"Found {len(all_open_orders_for_market)} Serum OpenOrders account(s) for market {market.fully_qualified_symbol}."
)
for open_orders in all_open_orders_for_market:
mango.output(open_orders)

View File

@ -38,6 +38,11 @@ parser.add_argument(
type=PublicKey,
help="address of the specific account to use, if more than one available",
)
parser.add_argument(
"--destination-wallet",
type=PublicKey,
help="if specified, the wallet to which the withdrawal should be sent. (Defaults to the current wallet.)",
)
parser.add_argument(
"--allow-borrow",
action="store_true",
@ -160,8 +165,9 @@ with mango.ContextBuilder.from_command_line_parameters(args) as context:
if args.dry_run:
mango.output("Dry run - not sending transaction")
else:
destination = args.destination_wallet or wallet.address
signatures = account.withdraw(
context, wallet, withdrawal_value, args.allow_borrow
context, wallet, destination, withdrawal_value, args.allow_borrow
)
if args.wait:

View File

@ -24,6 +24,7 @@ from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount
from .cache import Cache, PerpMarketCache, RootBankCache, MarketCache
from .combinableinstructions import CombinableInstructions
from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context
from .encoding import encode_key
from .group import Group, GroupSlot, GroupSlotPerpMarket
@ -638,12 +639,25 @@ class Account(AddressableAccount):
self,
context: Context,
wallet: Wallet,
destination: PublicKey,
value: InstrumentValue,
allow_borrow: bool,
) -> typing.Sequence[str]:
destination_info: typing.Optional[AccountInfo] = AccountInfo.load(
context, destination
)
if destination_info is None:
raise Exception(f"Could not find wallet at address {destination}.")
if destination_info.owner != SYSTEM_PROGRAM_ADDRESS:
# This is not a root wallet account
raise Exception(
f"Can't withdraw to address {destination} - not a wallet address."
)
token: Token = Token.ensure(value.token)
token_account = TokenAccount.fetch_largest_for_owner_and_token(
context, wallet.address, token
context, destination, token
)
withdrawal_token_account: TokenAccount
@ -653,7 +667,7 @@ class Account(AddressableAccount):
create_ata,
token_account,
) = build_create_associated_instructions_and_account(
context, wallet, wallet.address, token
context, wallet, destination, token
)
withdrawal_token_account = TokenAccount(

View File

@ -16,12 +16,15 @@
import logging
import typing
from decimal import Decimal
from solana.blockhash import Blockhash
from solana.keypair import Keypair
from solana.publickey import PublicKey
from solana.transaction import Transaction, TransactionInstruction
from solana.utils import shortvec_encoding as shortvec
from mango.constants import SOL_DECIMAL_DIVISOR
from .context import Context
from .instructionreporter import InstructionReporter
from .wallet import Wallet
@ -314,6 +317,38 @@ class CombinableInstructions:
) -> typing.Sequence[str]:
return self.execute(context, on_exception_continue)
def cost_to_execute(self, context: Context) -> Decimal:
# getFees() is depracated and will be replaced by getFeeForMessage() at some point.
# getFeeForMessage() is not fully available yet though.
fee_response = context.client.compatible_client.get_fees()
# fee_response should look like:
# {
# "jsonrpc": "2.0",
# "result": {
# "context": {
# "slot": 1
# },
# "value": {
# "blockhash": "CSymwgTNX1j3E4qhKfJAUE41nBWEwXufoYryPbkde5RR",
# "feeCalculator": {
# "lamportsPerSignature": 5000
# },
# "lastValidSlot": 297,
# "lastValidBlockHeight": 296
# }
# },
# "id": 1
# }
number_of_signatures = len(self.signers)
lamports_per_signature = fee_response["result"]["value"]["feeCalculator"][
"lamportsPerSignature"
]
fee_in_lamports = Decimal(number_of_signatures) * Decimal(
lamports_per_signature
)
return fee_in_lamports / SOL_DECIMAL_DIVISOR
def __str__(self) -> str:
report: typing.List[str] = []
for index, signer in enumerate(self.signers):

View File

@ -45,14 +45,14 @@ class PerpToSpotHedger(Hedger):
underlying_market.quote != hedging_market.quote
):
raise Exception(
f"Market {hedging_market.symbol} cannot be used to hedge market {underlying_market.symbol}."
f"Market {hedging_market.fully_qualified_symbol} cannot be used to hedge market {underlying_market.fully_qualified_symbol}."
)
if not mango.Instrument.symbols_match(
target_balance.symbol, hedging_market.base.symbol
):
raise Exception(
f"Cannot target {target_balance.symbol} when hedging on {hedging_market.symbol}"
f"Cannot target {target_balance.symbol} when hedging on {hedging_market.fully_qualified_symbol}"
)
self.underlying_market: mango.PerpMarket = underlying_market
@ -124,7 +124,7 @@ class PerpToSpotHedger(Hedger):
perp_position_rounded + token_balance_rounded - self.target_balance
)
self._logger.debug(
f"Delta from {self.underlying_market.symbol} to {self.hedging_market.symbol} is {delta:,.8f} {basket_token.base_instrument.symbol}, action threshold is: {self.action_threshold}"
f"Delta from {self.underlying_market.fully_qualified_symbol} to {self.hedging_market.fully_qualified_symbol} is {delta:,.8f} {basket_token.base_instrument.symbol}, action threshold is: {self.action_threshold}"
)
if abs(delta) > self.action_threshold:
@ -151,14 +151,14 @@ class PerpToSpotHedger(Hedger):
side, adjusted_price, quantity, mango.OrderType.IOC
)
self._logger.info(
f"Hedging perp position {perp_position} and token balance {token_balance} with {side} of {quantity:,.8f} at {up_or_down} ({model_state.price}) {adjusted_price:,.8f} on {self.hedging_market.symbol}\n\t{order}"
f"Hedging perp position {perp_position} and token balance {token_balance} with {side} of {quantity:,.8f} at {up_or_down} ({model_state.price}) {adjusted_price:,.8f} on {self.hedging_market.fully_qualified_symbol}\n\t{order}"
)
try:
self.market_operations.place_order(order)
self.pause_counter = 0
except Exception:
self._logger.error(
f"[{context.name}] Failed to hedge on {self.hedging_market.symbol} using order {order} - {traceback.format_exc()}"
f"[{context.name}] Failed to hedge on {self.hedging_market.fully_qualified_symbol} using order {order} - {traceback.format_exc()}"
)
raise
@ -181,4 +181,4 @@ class PerpToSpotHedger(Hedger):
self.pulse_error.on_next(exception)
def __str__(self) -> str:
return f"« PerpToSpotHedger for underlying '{self.underlying_market.symbol}', hedging on '{self.hedging_market.symbol}' »"
return f"« PerpToSpotHedger for underlying '{self.underlying_market.fully_qualified_symbol}', hedging on '{self.hedging_market.fully_qualified_symbol}' »"

View File

@ -81,7 +81,7 @@ class MarketMaker:
existing_orders = model_state.current_orders()
self._logger.debug(
f"""Before reconciliation: all owned orders on current orderbook [{model_state.market.symbol}]:
f"""Before reconciliation: all owned orders on current orderbook [{model_state.market.fully_qualified_symbol}]:
{mango.indent_collection_as_str(existing_orders)}"""
)
reconciled = self.order_reconciler.reconcile(
@ -106,14 +106,16 @@ Ignore:
):
ids = [f"{ord.id} / {ord.client_id}" for ord in reconciled.to_cancel]
self._logger.info(
f"Cancelling all orders on {self.market.symbol} - currently {len(ids)}: {ids}"
f"Cancelling all orders on {self.market.fully_qualified_symbol} - currently {len(ids)}: {ids}"
)
cancellations = (
self.market_instruction_builder.build_cancel_all_orders_instructions()
)
else:
for to_cancel in reconciled.to_cancel:
self._logger.info(f"Cancelling {self.market.symbol} {to_cancel}")
self._logger.info(
f"Cancelling {self.market.fully_qualified_symbol} {to_cancel}"
)
cancel = (
self.market_instruction_builder.build_cancel_order_instructions(
to_cancel, ok_if_missing=True
@ -129,7 +131,7 @@ Ignore:
)
self._logger.info(
f"Placing {self.market.symbol} {to_place_with_client_id}"
f"Placing {self.market.fully_qualified_symbol} {to_place_with_client_id}"
)
place_order = (
self.market_instruction_builder.build_place_order_instructions(
@ -138,8 +140,14 @@ Ignore:
)
place_orders += place_order
accounts_to_crank = list(model_state.accounts_to_crank)
if self.market_instruction_builder.open_orders_address is not None:
accounts_to_crank += [
self.market_instruction_builder.open_orders_address
]
crank = self.market_instruction_builder.build_crank_instructions(
model_state.accounts_to_crank
accounts_to_crank
)
settle = self.market_instruction_builder.build_settle_instructions()
@ -176,7 +184,7 @@ Ignore:
self.pulse_error.on_next(exception)
def __str__(self) -> str:
return f"""« MarketMaker for market '{self.market.symbol}' »"""
return f"""« MarketMaker for market '{self.market.fully_qualified_symbol}' »"""
def __repr__(self) -> str:
return f"{self}"

View File

@ -61,7 +61,7 @@ class WebsocketModelStateBuilder(ModelStateBuilder):
return self.model_state
def __str__(self) -> str:
return f"« WebsocketModelStateBuilder for market '{self.model_state.market.symbol}' »"
return f"« WebsocketModelStateBuilder for market '{self.model_state.market.fully_qualified_symbol}' »"
# # 🥭 PollingModelStateBuilder class
@ -247,9 +247,7 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
)
def __str__(self) -> str:
return (
f"""« SerumPollingModelStateBuilder for market '{self.market.symbol}' »"""
)
return f"""« SerumPollingModelStateBuilder for market '{self.market.fully_qualified_symbol}' »"""
# # 🥭 SpotPollingModelStateBuilder class
@ -372,7 +370,7 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
)
def __str__(self) -> str:
return f"""« SpotPollingModelStateBuilder for market '{self.market.symbol}' »"""
return f"""« SpotPollingModelStateBuilder for market '{self.market.fully_qualified_symbol}' »"""
# # 🥭 PerpPollingModelStateBuilder class
@ -490,4 +488,4 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder):
)
def __str__(self) -> str:
return f"""« PerpPollingModelStateBuilder for market '{self.market.symbol}' »"""
return f"""« PerpPollingModelStateBuilder for market '{self.market.fully_qualified_symbol}' »"""

View File

@ -97,7 +97,9 @@ def _polling_model_state_builder_factory(
group, account, mango.PerpMarket.ensure(market), oracle
)
else:
raise Exception(f"Could not determine type of market {market.symbol}")
raise Exception(
f"Could not determine type of market {market.fully_qualified_symbol}: {market}"
)
def _polling_serum_model_state_builder_factory(
@ -132,7 +134,7 @@ def _polling_serum_model_state_builder_factory(
)
if len(all_open_orders) == 0:
raise Exception(
f"Could not find serum openorders account owned by {wallet.address} for market {market.symbol}."
f"Could not find serum openorders account owned by {wallet.address} for market {market.fully_qualified_symbol}."
)
return SerumPollingModelStateBuilder(
all_open_orders[0].address,
@ -160,7 +162,7 @@ def _polling_spot_model_state_builder_factory(
all_open_orders_addresses: typing.Sequence[PublicKey] = account.spot_open_orders
if open_orders_address is None:
raise Exception(
f"Could not find spot openorders in account {account.address} for market {market.symbol}."
f"Could not find spot openorders in account {account.address} for market {market.fully_qualified_symbol}."
)
return SpotPollingModelStateBuilder(
open_orders_address,
@ -350,7 +352,9 @@ def _websocket_model_state_builder_factory(
context, websocket_manager, health_check, perp_market
)
else:
raise Exception(f"Could not determine type of market {market.symbol}")
raise Exception(
f"Could not determine type of market {market.fully_qualified_symbol} - {market}"
)
model_state = ModelState(
order_owner,

View File

@ -60,6 +60,7 @@ from .orders import Order, OrderBook, OrderType, Side
class MarketInstructionBuilder(metaclass=abc.ABCMeta):
def __init__(self) -> None:
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.open_orders_address: typing.Optional[PublicKey] = None
@abc.abstractmethod
def build_cancel_order_instructions(
@ -120,7 +121,7 @@ class MarketOperations(metaclass=abc.ABCMeta):
@property
def symbol(self) -> str:
return self.market.symbol
return self.market.fully_qualified_symbol
@property
def inventory_source(self) -> InventorySource:
@ -269,7 +270,9 @@ class NullMarketOperations(MarketOperations):
return []
def load_orderbook(self) -> OrderBook:
return OrderBook(self.market.symbol, NullLotSizeConverter(), [], [])
return OrderBook(
self.market.fully_qualified_symbol, NullLotSizeConverter(), [], []
)
def load_my_orders(self, include_expired: bool = False) -> typing.Sequence[Order]:
return []
@ -287,4 +290,4 @@ class NullMarketOperations(MarketOperations):
return SYSTEM_PROGRAM_ADDRESS
def __str__(self) -> str:
return f"""« NullMarketOperations [{self.market.symbol}] »"""
return f"""« NullMarketOperations [{self.market.fully_qualified_symbol}] »"""

View File

@ -83,6 +83,13 @@ class Market(metaclass=abc.ABCMeta):
def symbol(self) -> str:
return f"{self.base.symbol}/{self.quote.symbol}"
@property
@abc.abstractproperty
def fully_qualified_symbol(self) -> str:
raise NotImplementedError(
"Market.fully_qualified_symbol is not implemented on the base type."
)
def __str__(self) -> str:
return f"« Market {self.symbol} »"

View File

@ -140,7 +140,7 @@ class ModelState:
)
def __str__(self) -> str:
return f"""« ModelState for market '{self.market.symbol}'
return f"""« ModelState for market '{self.market.fully_qualified_symbol}'
Group: {self.group_watcher.latest.address}
Account: {self.account_watcher.latest.address}
Price: {self.price_watcher.latest}

View File

@ -23,7 +23,7 @@ from datetime import datetime
from decimal import Decimal
from .context import Context
from .markets import Market
from .loadedmarket import LoadedMarket
# # 🥭 Oracles
@ -56,7 +56,7 @@ class OracleSource:
provider_name: str,
source_name: str,
supports: SupportedOracleFeature,
market: Market,
market: LoadedMarket,
) -> None:
self.provider_name = provider_name
self.source_name = source_name
@ -64,7 +64,7 @@ class OracleSource:
self.market = market
def __str__(self) -> str:
return f"« OracleSource '{self.source_name}' from '{self.provider_name}' for market '{self.market.symbol}' [{self.supports}] »"
return f"« OracleSource '{self.source_name}' from '{self.provider_name}' for market '{self.market.fully_qualified_symbol}' [{self.supports}] »"
def __repr__(self) -> str:
return f"{self}"
@ -79,7 +79,7 @@ class Price:
self,
source: OracleSource,
timestamp: datetime,
market: Market,
market: LoadedMarket,
top_bid: Decimal,
mid_price: Decimal,
top_ask: Decimal,
@ -87,7 +87,7 @@ class Price:
) -> None:
self.source: OracleSource = source
self.timestamp: datetime = timestamp
self.market: Market = market
self.market: LoadedMarket = market
self.top_bid: Decimal = top_bid
self.mid_price: Decimal = mid_price
self.top_ask: Decimal = top_ask
@ -101,7 +101,7 @@ class Price:
confidence = ""
if self.source.supports & SupportedOracleFeature.CONFIDENCE:
confidence = f" +/- {self.confidence:,.8f}"
return f"« Price [{self.source.provider_name}] {self.market.symbol} at {self.timestamp}: {self.mid_price:,.8f}{confidence} »"
return f"« Price [{self.source.provider_name}] {self.market.fully_qualified_symbol} at {self.timestamp}: {self.mid_price:,.8f}{confidence} »"
def __repr__(self) -> str:
return f"{self}"
@ -112,7 +112,7 @@ class Price:
# Derived versions of this class can fetch prices for a specific market.
#
class Oracle(metaclass=abc.ABCMeta):
def __init__(self, name: str, market: Market) -> None:
def __init__(self, name: str, market: LoadedMarket) -> None:
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.name = name
self.market = market
@ -136,7 +136,7 @@ class Oracle(metaclass=abc.ABCMeta):
)
def __str__(self) -> str:
return f"« Oracle {self.name} [{self.market.symbol}] »"
return f"« Oracle {self.name} [{self.market.fully_qualified_symbol}] »"
def __repr__(self) -> str:
return f"{self}"
@ -152,7 +152,7 @@ class OracleProvider(metaclass=abc.ABCMeta):
@abc.abstractmethod
def oracle_for_market(
self, context: Context, market: Market
self, context: Context, market: LoadedMarket
) -> typing.Optional[Oracle]:
raise NotImplementedError(
"OracleProvider.create_oracle_for_market() is not implemented on the base type."

View File

@ -25,7 +25,7 @@ from rx.subject.subject import Subject
from ...context import Context
from ...datetimes import utc_now, datetime_from_timestamp
from ...markets import Market
from ...loadedmarket import LoadedMarket
from ...observables import Disposable, DisposeWrapper
from ...oracle import (
Oracle,
@ -61,10 +61,10 @@ FtxOracleConfidence: Decimal = Decimal(0)
# Implements the `Oracle` abstract base class specialised to the Ftx Network.
#
class FtxOracle(Oracle):
def __init__(self, market: Market, ftx_symbol: str) -> None:
name = f"Ftx Oracle for {market.symbol} / {ftx_symbol}"
def __init__(self, market: LoadedMarket, ftx_symbol: str) -> None:
name = f"Ftx Oracle for {market.fully_qualified_symbol} / {ftx_symbol}"
super().__init__(name, market)
self.market: Market = market
self.market: LoadedMarket = market
self.ftx_symbol: str = ftx_symbol
features: SupportedOracleFeature = (
SupportedOracleFeature.MID_PRICE | SupportedOracleFeature.TOP_BID_AND_OFFER
@ -146,7 +146,7 @@ class FtxOracleProvider(OracleProvider):
super().__init__("Ftx Oracle Factory")
def oracle_for_market(
self, context: Context, market: Market
self, context: Context, market: LoadedMarket
) -> typing.Optional[Oracle]:
symbol = self._market_symbol_to_ftx_symbol(market.symbol)
return FtxOracle(market, symbol)

View File

@ -23,7 +23,6 @@ from decimal import Decimal
from ...context import Context
from ...datetimes import utc_now
from ...loadedmarket import LoadedMarket
from ...markets import Market
from ...observables import observable_pipeline_error_reporter
from ...oracle import (
Oracle,
@ -33,7 +32,6 @@ from ...oracle import (
SupportedOracleFeature,
)
from ...orders import OrderBook
from ...porcelain import market as porcelain_market
# # 🥭 Market
@ -58,7 +56,7 @@ MarketOracleConfidence: Decimal = Decimal(0)
#
class MarketOracle(Oracle):
def __init__(self, market: LoadedMarket):
name = f"Market Oracle for {market.symbol}"
name = f"Market Oracle for {market.fully_qualified_symbol}"
super().__init__(name, market)
self.loaded_market: LoadedMarket = market
features: SupportedOracleFeature = SupportedOracleFeature.TOP_BID_AND_OFFER
@ -116,10 +114,9 @@ class MarketOracleProvider(OracleProvider):
super().__init__("Market Oracle Factory")
def oracle_for_market(
self, context: Context, market: Market
self, context: Context, market: LoadedMarket
) -> typing.Optional[Oracle]:
loaded_market: LoadedMarket = porcelain_market(context, market.symbol)
return MarketOracle(loaded_market)
return MarketOracle(market)
def all_available_symbols(self, context: Context) -> typing.Sequence[str]:
all_markets = context.market_lookup.all_markets()

View File

@ -25,7 +25,7 @@ from solana.publickey import PublicKey
from ...accountinfo import AccountInfo
from ...context import Context
from ...datetimes import utc_now
from ...markets import Market
from ...loadedmarket import LoadedMarket
from ...observables import observable_pipeline_error_reporter
from ...oracle import (
Oracle,
@ -74,11 +74,13 @@ from .layouts import (
class PythOracle(Oracle):
def __init__(self, context: Context, market: Market, product_data: typing.Any):
name = f"Pyth Oracle for {market.symbol}"
def __init__(
self, context: Context, market: LoadedMarket, product_data: typing.Any
):
name = f"Pyth Oracle for {market.fully_qualified_symbol}"
super().__init__(name, market)
self.context: Context = context
self.market: Market = market
self.market: LoadedMarket = market
self.product_data: typing.Any = product_data
self.address: PublicKey = product_data.address
features: SupportedOracleFeature = (
@ -153,7 +155,9 @@ class PythOracleProvider(OracleProvider):
super().__init__(f"Pyth Oracle Factory [{self.address}]")
self.context: Context = context
def oracle_for_market(self, _: Context, market: Market) -> typing.Optional[Oracle]:
def oracle_for_market(
self, _: Context, market: LoadedMarket
) -> typing.Optional[Oracle]:
pyth_symbol = self._market_symbol_to_pyth_symbol(market.symbol)
products = self._fetch_all_pyth_products(self.context, self.address)
for product in products:

View File

@ -25,7 +25,6 @@ from ...cache import Cache
from ...context import Context
from ...datetimes import utc_now
from ...loadedmarket import LoadedMarket
from ...markets import Market
from ...observables import observable_pipeline_error_reporter
from ...oracle import (
Oracle,
@ -35,7 +34,6 @@ from ...oracle import (
SupportedOracleFeature,
)
from ...perpmarket import PerpMarket
from ...porcelain import market as porcelain_market
from ...spotmarket import SpotMarket
@ -60,8 +58,10 @@ StubOracleConfidence: Decimal = Decimal(0)
class StubOracle(Oracle):
def __init__(self, market: Market, index: int, cache_address: PublicKey) -> None:
name = f"Stub Oracle for {market.symbol}"
def __init__(
self, market: LoadedMarket, index: int, cache_address: PublicKey
) -> None:
name = f"Stub Oracle for {market.fully_qualified_symbol}"
super().__init__(name, market)
self.index: int = index
self.cache_address: PublicKey = cache_address
@ -116,19 +116,18 @@ class StubOracleProvider(OracleProvider):
super().__init__("Stub Oracle Factory")
def oracle_for_market(
self, context: Context, market: Market
self, context: Context, market: LoadedMarket
) -> typing.Optional[Oracle]:
loaded_market: LoadedMarket = porcelain_market(context, market.symbol)
if SpotMarket.isa(loaded_market):
spot_market = SpotMarket.ensure(loaded_market)
if SpotMarket.isa(market):
spot_market = SpotMarket.ensure(market)
spot_index: int = spot_market.group.slot_by_spot_market_address(
loaded_market.address
market.address
).index
return StubOracle(spot_market, spot_index, spot_market.group.cache)
elif PerpMarket.isa(loaded_market):
perp_market = PerpMarket.ensure(loaded_market)
elif PerpMarket.isa(market):
perp_market = PerpMarket.ensure(market)
perp_index: int = perp_market.group.slot_by_perp_market_address(
loaded_market.address
market.address
).index
return StubOracle(perp_market, perp_index, perp_market.group.cache)

View File

@ -158,13 +158,19 @@ class PerpMarket(LoadedMarket):
@staticmethod
def ensure(market: Market) -> "PerpMarket":
if not PerpMarket.isa(market):
raise Exception(f"Market for {market.symbol} is not a Perp market")
raise Exception(
f"Market for {market.fully_qualified_symbol} is not a Perp market"
)
return typing.cast(PerpMarket, market)
@property
def symbol(self) -> str:
return f"{self.base.symbol}-PERP"
@property
def fully_qualified_symbol(self) -> str:
return f"perp:{self.symbol}"
@property
def group(self) -> Group:
return self.underlying_perp_market.group
@ -286,10 +292,6 @@ class PerpMarketInstructionBuilder(MarketInstructionBuilder):
def build_cancel_order_instructions(
self, order: Order, ok_if_missing: bool = False
) -> CombinableInstructions:
if self.perp_market.underlying_perp_market is None:
raise Exception(
f"PerpMarket {self.perp_market.symbol} has not been loaded."
)
return build_perp_cancel_order_instructions(
self.context,
self.wallet,
@ -300,10 +302,6 @@ class PerpMarketInstructionBuilder(MarketInstructionBuilder):
)
def build_place_order_instructions(self, order: Order) -> CombinableInstructions:
if self.perp_market.underlying_perp_market is None:
raise Exception(
f"PerpMarket {self.perp_market.symbol} has not been loaded."
)
return build_perp_place_order_instructions(
self.context,
self.wallet,
@ -327,11 +325,6 @@ class PerpMarketInstructionBuilder(MarketInstructionBuilder):
def build_crank_instructions(
self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)
) -> CombinableInstructions:
if self.perp_market.underlying_perp_market is None:
raise Exception(
f"PerpMarket {self.perp_market.symbol} has not been loaded."
)
distinct_addresses: typing.List[PublicKey] = [self.account.address]
for address in addresses:
if address not in distinct_addresses:
@ -371,10 +364,6 @@ class PerpMarketInstructionBuilder(MarketInstructionBuilder):
def build_cancel_all_orders_instructions(
self, limit: Decimal = Decimal(32)
) -> CombinableInstructions:
if self.perp_market.underlying_perp_market is None:
raise Exception(
f"PerpMarket {self.perp_market.symbol} has not been loaded."
)
return build_perp_cancel_all_orders_instructions(
self.context,
self.wallet,
@ -419,14 +408,10 @@ class PerpMarketOperations(MarketOperations):
def perp_market(self) -> PerpMarket:
return self.market_instruction_builder.perp_market
@property
def market_name(self) -> str:
return self.perp_market.symbol
def cancel_order(
self, order: Order, ok_if_missing: bool = False
) -> typing.Sequence[str]:
self._logger.info(f"Cancelling {self.market_name} order {order}.")
self._logger.info(f"Cancelling {self.symbol} order {order}.")
signers: CombinableInstructions = CombinableInstructions.from_wallet(
self.wallet
)
@ -447,7 +432,7 @@ class PerpMarketOperations(MarketOperations):
self.wallet
)
order_with_client_id: Order = order.with_update(client_id=client_id)
self._logger.info(f"Placing {self.market_name} order {order_with_client_id}.")
self._logger.info(f"Placing {self.symbol} order {order_with_client_id}.")
place: CombinableInstructions = (
self.market_instruction_builder.build_place_order_instructions(
order_with_client_id
@ -507,7 +492,7 @@ class PerpMarketOperations(MarketOperations):
)
def __str__(self) -> str:
return f"""« PerpMarketOperations [{self.market_name}] »"""
return f"""« PerpMarketOperations [{self.symbol}] »"""
# # 🥭 PerpMarketStub class
@ -534,6 +519,10 @@ class PerpMarketStub(Market):
)
self.group_address: PublicKey = group_address
@property
def fully_qualified_symbol(self) -> str:
return f"perp:{self.symbol}"
def load(
self, context: Context, group: typing.Optional[Group] = None
) -> PerpMarket:

View File

@ -120,7 +120,7 @@ def instruction_builder(
) -> MarketInstructionBuilder:
loaded_market: LoadedMarket = market(context, symbol)
if dry_run:
return NullMarketInstructionBuilder(loaded_market.symbol)
return NullMarketInstructionBuilder(loaded_market.fully_qualified_symbol)
if SerumMarket.isa(loaded_market):
return SerumMarketInstructionBuilder.load(

View File

@ -89,9 +89,15 @@ class SerumMarket(LoadedMarket):
@staticmethod
def ensure(market: Market) -> "SerumMarket":
if not SerumMarket.isa(market):
raise Exception(f"Market for {market.symbol} is not a Serum market")
raise Exception(
f"Market for {market.fully_qualified_symbol} is not a Serum market"
)
return typing.cast(SerumMarket, market)
@property
def fully_qualified_symbol(self) -> str:
return f"serum:{self.symbol}"
@property
def bids_address(self) -> PublicKey:
return self.underlying_serum_market.state.bids()
@ -426,7 +432,9 @@ class SerumMarketOperations(MarketOperations):
def cancel_order(
self, order: Order, ok_if_missing: bool = False
) -> typing.Sequence[str]:
self._logger.info(f"Cancelling {self.serum_market.symbol} order {order}.")
self._logger.info(
f"Cancelling {self.serum_market.fully_qualified_symbol} order {order}."
)
signers: CombinableInstructions = CombinableInstructions.from_wallet(
self.wallet
)
@ -463,7 +471,7 @@ class SerumMarketOperations(MarketOperations):
order_type=order.order_type,
)
self._logger.info(
f"Placing {self.serum_market.symbol} order {order_with_client_id}."
f"Placing {self.serum_market.fully_qualified_symbol} order {order_with_client_id}."
)
place: CombinableInstructions = (
self.market_instruction_builder.build_place_order_instructions(
@ -546,7 +554,7 @@ class SerumMarketOperations(MarketOperations):
)
def __str__(self) -> str:
return f"""« SerumMarketOperations [{self.serum_market.symbol}] »"""
return f"""« SerumMarketOperations [{self.serum_market.fully_qualified_symbol}] »"""
# # 🥭 SerumMarketStub class
@ -573,6 +581,10 @@ class SerumMarketStub(Market):
self.base: Token = base
self.quote: Token = quote
@property
def fully_qualified_symbol(self) -> str:
return f"serum:{self.symbol}"
def load(self, context: Context) -> SerumMarket:
underlying_serum_market: PySerumMarket = PySerumMarket.load(
context.client.compatible_client,

View File

@ -252,7 +252,7 @@ class SimpleMarketMaker:
)
def __str__(self) -> str:
return f"""« SimpleMarketMaker for market '{self.market.symbol}' »"""
return f"""« SimpleMarketMaker for market '{self.market.fully_qualified_symbol}' »"""
def __repr__(self) -> str:
return f"{self}"

View File

@ -93,9 +93,15 @@ class SpotMarket(LoadedMarket):
@staticmethod
def ensure(market: Market) -> "SpotMarket":
if not SpotMarket.isa(market):
raise Exception(f"Market for {market.symbol} is not a Spot market")
raise Exception(
f"Market for {market.fully_qualified_symbol} is not a Spot market"
)
return typing.cast(SpotMarket, market)
@property
def fully_qualified_symbol(self) -> str:
return f"spot:{self.symbol}"
@property
def bids_address(self) -> PublicKey:
return self.underlying_serum_market.state.bids()
@ -410,7 +416,7 @@ class SpotMarketInstructionBuilder(MarketInstructionBuilder):
)
def __str__(self) -> str:
return f"« SpotMarketInstructionBuilder [{self.spot_market.symbol}] »"
return f"« SpotMarketInstructionBuilder [{self.spot_market.fully_qualified_symbol}] »"
# # 🥭 SpotMarketOperations class
@ -459,7 +465,9 @@ class SpotMarketOperations(MarketOperations):
def cancel_order(
self, order: Order, ok_if_missing: bool = False
) -> typing.Sequence[str]:
self._logger.info(f"Cancelling {self.spot_market.symbol} order {order}.")
self._logger.info(
f"Cancelling {self.spot_market.fully_qualified_symbol} order {order}."
)
signers: CombinableInstructions = CombinableInstructions.from_wallet(
self.wallet
)
@ -486,7 +494,9 @@ class SpotMarketOperations(MarketOperations):
order_with_client_id: Order = order.with_update(
client_id=client_id
).with_update(owner=self.open_orders_address or SYSTEM_PROGRAM_ADDRESS)
self._logger.info(f"Placing {self.spot_market.symbol} order {order}.")
self._logger.info(
f"Placing {self.spot_market.fully_qualified_symbol} order {order}."
)
place: CombinableInstructions = (
self.market_instruction_builder.build_place_order_instructions(
order_with_client_id
@ -577,7 +587,7 @@ class SpotMarketOperations(MarketOperations):
)
def __str__(self) -> str:
return f"« SpotMarketOperations [{self.spot_market.symbol}] »"
return f"« SpotMarketOperations [{self.spot_market.fully_qualified_symbol}] »"
# # 🥭 SpotMarketStub class
@ -606,6 +616,10 @@ class SpotMarketStub(Market):
self.quote: Token = quote
self.group_address: PublicKey = group_address
@property
def fully_qualified_symbol(self) -> str:
return f"spot:{self.symbol}"
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(

View File

@ -31,7 +31,7 @@ from .instructions import build_serum_create_openorders_instructions
from .instrumentvalue import InstrumentValue
from .inventory import Inventory
from .loadedmarket import LoadedMarket
from .markets import Market, InventorySource
from .markets import InventorySource
from .modelstate import EventQueue
from .observables import Disposable, LatestItemObserverSubscriber
from .openorders import OpenOrders
@ -137,7 +137,7 @@ def build_spot_open_orders_watcher(
)
open_orders_address = market_operations.create_openorders()
logging.info(
f"Created {spot_market.symbol} OpenOrders at: {open_orders_address}"
f"Created {spot_market.fully_qualified_symbol} OpenOrders at: {open_orders_address}"
)
spot_open_orders_subscription = WebSocketAccountSubscription[OpenOrders](
@ -193,7 +193,7 @@ def build_serum_open_orders_watcher(
open_orders_address = create_open_orders.signers[0].public_key
logging.info(
f"Creating OpenOrders account for market {serum_market.symbol} at {open_orders_address}."
f"Creating OpenOrders account for market {serum_market.fully_qualified_symbol} at {open_orders_address}."
)
signers: CombinableInstructions = CombinableInstructions.from_wallet(wallet)
transaction_ids = (signers + create_open_orders).execute(context)
@ -262,13 +262,13 @@ def build_price_watcher(
health_check: HealthCheck,
disposer: Disposable,
provider_name: str,
market: Market,
market: LoadedMarket,
) -> LatestItemObserverSubscriber[Price]:
oracle_provider: OracleProvider = create_oracle_provider(context, provider_name)
oracle = oracle_provider.oracle_for_market(context, market)
if oracle is None:
raise Exception(
f"Could not find oracle for market {market.symbol} from provider {provider_name}."
f"Could not find oracle for market {market.fully_qualified_symbol} from provider {provider_name}."
)
initial_price = oracle.fetch_price(context)
@ -373,7 +373,7 @@ def build_orderbook_watcher(
or orderbook_infos[1] is None
):
raise Exception(
f"Could not find {market.symbol} order book at addresses {orderbook_addresses}."
f"Could not find {market.fully_qualified_symbol} order book at addresses {orderbook_addresses}."
)
initial_orderbook: OrderBook = market.parse_account_infos_to_orderbook(

View File

@ -182,6 +182,10 @@ def fake_loaded_market(
base_lot_size: Decimal = Decimal(1), quote_lot_size: Decimal = Decimal(1)
) -> mango.LoadedMarket:
class FakeLoadedMarket(mango.LoadedMarket):
@property
def fully_qualified_symbol(self) -> str:
return "full:MARKET/SYMBOL"
@property
def bids_address(self) -> PublicKey:
return fake_seeded_public_key("bids_address")
@ -270,7 +274,7 @@ def fake_order_id(index: int, price: int) -> int:
def fake_price(
market: mango.Market = fake_loaded_market(),
market: mango.LoadedMarket = fake_loaded_market(),
price: Decimal = Decimal(100),
bid: Decimal = Decimal(99),
ask: Decimal = Decimal(101),

View File

@ -10,7 +10,9 @@ def test_market_symbol_matching() -> None:
assert mango.Market.symbols_match("eth/usdc", "eth/usdc")
assert mango.Market.symbols_match("btc/usdc", "BTC/USDC")
assert mango.Market.symbols_match("ETH/USDC", "eth/usdc")
assert mango.Market.symbols_match("serum:ETH/USDC", "serum:eth/usdc")
assert not mango.Market.symbols_match("ETH/USDC", "BTC/USDC")
assert not mango.Market.symbols_match("serum:ETH/USDC", "spot:eth/usdc")
def test_serum_market_lookup() -> None: