diff --git a/README.md b/README.md index 6a1b329..e670f75 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ market = mango.ensure_market_loaded(context, stub) market_operations = mango.create_market_operations(context, wallet, account, market, dry_run=False) print("Initial order book:\n\t", market_operations.load_orderbook()) -print("Your current orders:\n\t", market_operations.load_my_orders()) +print("Your current orders:\n\t", market_operations.load_my_orders(include_expired=True)) # Go on - try to buy 1 SOL-PERP contract for $10. order = mango.Order.from_basic_info(side=mango.Side.BUY, @@ -74,7 +74,7 @@ print("\n\nSleeping for 10 seconds...") time.sleep(10) print("\n\nOrder book (including our new order):\n", market_operations.load_orderbook()) -print("Your current orders:\n\t", market_operations.load_my_orders()) +print("Your current orders:\n\t", market_operations.load_my_orders(include_expired=True)) cancellation_signatures = market_operations.cancel_order(placed_order) print("\n\nCancellation signature:\n\t", cancellation_signatures) @@ -83,7 +83,7 @@ print("\n\nSleeping for 10 seconds...") time.sleep(10) print("\n\nOrder book (without our order):\n", market_operations.load_orderbook()) -print("Your current orders:\n\t", market_operations.load_my_orders()) +print("Your current orders:\n\t", market_operations.load_my_orders(include_expired=True)) ``` diff --git a/bin/cancel-my-orders b/bin/cancel-my-orders index da0ab55..f092e15 100755 --- a/bin/cancel-my-orders +++ b/bin/cancel-my-orders @@ -48,7 +48,7 @@ if market is None: market_operations = mango.create_market_operations( context, wallet, account, market, args.dry_run ) -orders = market_operations.load_my_orders() +orders = market_operations.load_my_orders(include_expired=True) if len(orders) == 0: mango.output(f"No open orders on {market.symbol}") else: diff --git a/bin/download-trades b/bin/download-trades index 4bd55ee..a6a9024 100755 --- a/bin/download-trades +++ b/bin/download-trades @@ -55,7 +55,7 @@ for account in accounts: filename = f"trade-history-{account.address}.csv" history: mango.TradeHistory = mango.TradeHistory() if args.most_recent_hours: - cutoff: datetime = datetime.utcnow() - timedelta(hours=args.most_recent_hours) + cutoff: datetime = mango.utc_now() - timedelta(hours=args.most_recent_hours) cutoff = cutoff.replace(tzinfo=timezone(offset=timedelta())) history.download_latest(context, account, cutoff) else: diff --git a/bin/marketmaker b/bin/marketmaker index 21cc43e..59465e9 100755 --- a/bin/marketmaker +++ b/bin/marketmaker @@ -90,7 +90,7 @@ parser.add_argument( parser.add_argument( "--existing-order-time-in-force-tolerance", type=Decimal, - default=Decimal("0.001"), + default=Decimal(0), help="tolerance in time-in-force when matching existing orders or cancelling/replacing", ) parser.add_argument( @@ -187,7 +187,7 @@ def cleanup( ) ) cancels: mango.CombinableInstructions = mango.CombinableInstructions.empty() - orders = market_operations.load_my_orders() + orders = market_operations.load_my_orders(include_expired=True) for order in orders: cancels += market_instruction_builder.build_cancel_order_instructions( order, ok_if_missing=True diff --git a/bin/show-my-orders b/bin/show-my-orders index 87cc927..c7e172c 100755 --- a/bin/show-my-orders +++ b/bin/show-my-orders @@ -29,6 +29,12 @@ parser.add_argument( default=False, help="runs as read-only and does not perform any transactions", ) +parser.add_argument( + "--include-expired", + action="store_true", + default=False, + help="show all owned transactions, even those that have expired", +) args: argparse.Namespace = mango.parse_args(parser) context = mango.ContextBuilder.from_command_line_parameters(args) @@ -45,7 +51,7 @@ if market is None: market_operations = mango.create_market_operations( context, wallet, account, market, args.dry_run ) -orders = market_operations.load_my_orders() +orders = market_operations.load_my_orders(include_expired=args.include_expired) mango.output(f"{len(orders)} order(s) to show.") for order in orders: mango.output(order) diff --git a/docs/MarketmakingIntroduction.md b/docs/MarketmakingIntroduction.md index b70efd3..933e66d 100644 --- a/docs/MarketmakingIntroduction.md +++ b/docs/MarketmakingIntroduction.md @@ -67,7 +67,7 @@ try: bid, ask = self.calculate_order_prices(price) buy_quantity, sell_quantity = self.calculate_order_quantities(price, inventory) - current_orders = self.market_operations.load_my_orders() + current_orders = self.market_operations.load_my_orders(include_expired=True) buy_orders = [order for order in current_orders if order.side == mango.Side.BUY] if self.orders_require_action(buy_orders, bid, buy_quantity): self._logger.info("Cancelling BUY orders.") diff --git a/mango/__init__.py b/mango/__init__.py index 1bc92b1..5ac2eb8 100644 --- a/mango/__init__.py +++ b/mango/__init__.py @@ -64,6 +64,8 @@ from .createmarketoperations import ( create_market_instruction_builder as create_market_instruction_builder, ) from .createmarketoperations import create_market_operations as create_market_operations +from .datetimes import local_now as local_now +from .datetimes import utc_now as utc_now from .encoding import decode_binary as decode_binary from .encoding import encode_binary as encode_binary from .encoding import encode_key as encode_key diff --git a/mango/client.py b/mango/client.py index e6807ce..68cde28 100644 --- a/mango/client.py +++ b/mango/client.py @@ -45,6 +45,7 @@ from solana.rpc.types import ( from solana.transaction import Transaction from .constants import SOL_DECIMAL_DIVISOR +from .datetimes import local_now from .instructionreporter import InstructionReporter from .logmessages import expand_log_messages from .text import indent_collection_as_str @@ -394,7 +395,7 @@ class TransactionWatcher: self.collector = collector def report_on_transaction(self) -> None: - started_at: datetime = datetime.now() + started_at: datetime = local_now() for pause in [ 0.1, 0.2, @@ -438,7 +439,7 @@ class TransactionWatcher: ): [status] = transaction_response["result"]["value"] if status is not None: - delta: timedelta = datetime.now() - started_at + delta: timedelta = local_now() - started_at time_taken: float = delta.seconds + delta.microseconds / 1000000 # value should be a dict that looks like: @@ -493,7 +494,7 @@ class TransactionWatcher: return time.sleep(pause) - delta = datetime.now() - started_at + delta = local_now() - started_at time_wasted_looking: float = delta.seconds + delta.microseconds / 1000000 self.collector.add_transaction( TransactionStatus( @@ -1163,15 +1164,15 @@ class BetterClient: f"Waiting up to {max_wait_in_seconds} seconds for {transaction_ids}." ) all_confirmed: typing.List[str] = [] - start_time: datetime = datetime.now() + start_time: datetime = local_now() cutoff: datetime = start_time + timedelta(seconds=max_wait_in_seconds) for transaction_id in transaction_ids: - while datetime.now() < cutoff: + while local_now() < cutoff: time.sleep(1) confirmed = self.get_confirmed_transaction(transaction_id) if confirmed is not None: self._logger.info( - f"Confirmed {transaction_id} after {datetime.now() - start_time} seconds." + f"Confirmed {transaction_id} after {local_now() - start_time} seconds." ) all_confirmed += [transaction_id] break diff --git a/mango/datetimes.py b/mango/datetimes.py new file mode 100644 index 0000000..062f910 --- /dev/null +++ b/mango/datetimes.py @@ -0,0 +1,37 @@ +# # ⚠ 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 datetime import datetime, timezone + +# # 🥭 Datetimes +# +# This file contains a few useful datetime functions. +# +# They exist solely to make clear what is being used when called. There are 2 general cases: +# 1. Logging/time tracking/user output - local time is what should be used. +# 2. Comparison with dates stored on-chain - UTC should be used. +# +# Getting a UTC time that is comparable with on-chain datetimes isn't intuitive, so putting +# it in one place and using that consistently should make it clearer in the calling code +# what is going on, without the unnecessary complications. +# + + +def local_now() -> datetime: + return datetime.now() + + +def utc_now() -> datetime: + return datetime.utcnow().astimezone(timezone.utc) diff --git a/mango/hedging/perptospothedger.py b/mango/hedging/perptospothedger.py index 89fc4b7..92d18f6 100644 --- a/mango/hedging/perptospothedger.py +++ b/mango/hedging/perptospothedger.py @@ -18,7 +18,6 @@ import mango import traceback import typing -from datetime import datetime from decimal import Decimal from .hedger import Hedger @@ -163,7 +162,7 @@ class PerpToSpotHedger(Hedger): ) raise - self.pulse_complete.on_next(datetime.now()) + self.pulse_complete.on_next(mango.local_now()) except ( mango.RateLimitException, mango.NodeIsBehindException, diff --git a/mango/liquidationprocessor.py b/mango/liquidationprocessor.py index e4f4b6f..aaf38f4 100644 --- a/mango/liquidationprocessor.py +++ b/mango/liquidationprocessor.py @@ -24,6 +24,7 @@ from decimal import Decimal from .account import Account from .accountliquidator import AccountLiquidator from .context import Context +from .datetimes import local_now from .group import Group from .instrumentvalue import InstrumentValue from .liquidatablereport import LiquidatableReport, LiquidatableState @@ -79,8 +80,8 @@ class LiquidationProcessor: LiquidationEvent ]() self.ripe_accounts: typing.Optional[typing.Sequence[Account]] = None - self.ripe_accounts_updated_at: datetime = datetime.now() - self.prices_updated_at: datetime = datetime.now() + self.ripe_accounts_updated_at: datetime = local_now() + self.prices_updated_at: datetime = local_now() self.state: LiquidationProcessorState = LiquidationProcessorState.STARTING self.state_change: EventSource[LiquidationProcessor] = EventSource[ LiquidationProcessor @@ -92,7 +93,7 @@ class LiquidationProcessor: ) self._check_update_recency("prices", self.prices_updated_at) self.ripe_accounts = ripe_accounts - self.ripe_accounts_updated_at = datetime.now() + self.ripe_accounts_updated_at = local_now() # If this is the first time through, mark ourselves as Healthy. if self.state == LiquidationProcessorState.STARTING: self.state = LiquidationProcessorState.HEALTHY @@ -160,7 +161,7 @@ class LiquidationProcessor: self._liquidate_all(group, prices, worthwhile) - self.prices_updated_at = datetime.now() + self.prices_updated_at = local_now() time_taken = time.time() - started_at self._logger.info( f"Check of all ripe 🥭 accounts complete. Time taken: {time_taken:.2f} seconds." @@ -215,7 +216,7 @@ class LiquidationProcessor: ) def _check_update_recency(self, name: str, last_updated_at: datetime) -> None: - how_long_ago_was_last_update = datetime.now() - last_updated_at + how_long_ago_was_last_update = local_now() - last_updated_at if how_long_ago_was_last_update > LiquidationProcessor._AGE_ERROR_THRESHOLD: self.state = LiquidationProcessorState.UNHEALTHY self.state_change.on_next(self) diff --git a/mango/marketmaking/marketmaker.py b/mango/marketmaking/marketmaker.py index e0724da..36b551d 100644 --- a/mango/marketmaking/marketmaker.py +++ b/mango/marketmaking/marketmaker.py @@ -155,7 +155,7 @@ Ignore: payer + cancellations + place_orders + crank + settle + redeem ).execute(context) - self.pulse_complete.on_next(datetime.now()) + self.pulse_complete.on_next(mango.local_now()) except ( mango.RateLimitException, mango.NodeIsBehindException, diff --git a/mango/marketoperations.py b/mango/marketoperations.py index 123d75d..c3e9ed5 100644 --- a/mango/marketoperations.py +++ b/mango/marketoperations.py @@ -139,7 +139,7 @@ class MarketOperations(metaclass=abc.ABCMeta): ) @abc.abstractmethod - def load_my_orders(self) -> typing.Sequence[Order]: + def load_my_orders(self, include_expired: bool = False) -> typing.Sequence[Order]: raise NotImplementedError( "MarketOperations.load_my_orders() is not implemented on the base type." ) @@ -228,7 +228,7 @@ class NullMarketOperations(MarketOperations): def load_orderbook(self) -> OrderBook: return OrderBook(self.market_name, NullLotSizeConverter(), [], []) - def load_my_orders(self) -> typing.Sequence[Order]: + def load_my_orders(self, include_expired: bool = False) -> typing.Sequence[Order]: return [] def settle(self) -> typing.Sequence[str]: diff --git a/mango/modelstate.py b/mango/modelstate.py index 0c6970f..57a7ad6 100644 --- a/mango/modelstate.py +++ b/mango/modelstate.py @@ -135,9 +135,9 @@ class ModelState: return self.event_queue_watcher.latest.accounts_to_crank def current_orders(self) -> typing.Sequence[Order]: - self.orderbook - all_orders = [*self.bids, *self.asks] - return list([o for o in all_orders if o.owner == self.order_owner]) + return self.orderbook.all_orders_for_owner( + self.order_owner, include_expired=True + ) def __str__(self) -> str: return f"""« ModelState for market '{self.market.symbol}' diff --git a/mango/observables.py b/mango/observables.py index a3062cb..91a7649 100644 --- a/mango/observables.py +++ b/mango/observables.py @@ -23,6 +23,7 @@ from datetime import datetime from rx.core.typing import Disposable from rxpy_backpressure import BackPressure +from .datetimes import local_now from .output import output @@ -83,7 +84,7 @@ class TimestampedPrintingObserverSubscriber(PrintingObserverSubscriber): super().__init__(report_no_output) def on_next(self, item: typing.Any) -> None: - super().on_next(f"{datetime.now()}: {item}") + super().on_next(f"{local_now()}: {item}") # # 🥭 CollectingObserverSubscriber class @@ -140,11 +141,11 @@ class LatestItemObserverSubscriber( def __init__(self, initial: TItem) -> None: super().__init__() self.latest: TItem = initial - self.update_timestamp: datetime = datetime.now() + self.update_timestamp: datetime = local_now() def on_next(self, item: TItem) -> None: self.latest = item - self.update_timestamp = datetime.now() + self.update_timestamp = local_now() def on_error(self, ex: Exception) -> None: pass diff --git a/mango/oracles/ftx/ftx.py b/mango/oracles/ftx/ftx.py index a82e0e1..54b8163 100644 --- a/mango/oracles/ftx/ftx.py +++ b/mango/oracles/ftx/ftx.py @@ -25,6 +25,7 @@ from decimal import Decimal from rx.subject.subject import Subject from ...context import Context +from ...datetimes import utc_now from ...market import Market from ...observables import DisposePropagator, DisposeWrapper from ...oracle import ( @@ -79,7 +80,7 @@ class FtxOracle(Oracle): return Price( self.source, - datetime.now(), + utc_now(), self.market, bid, price, diff --git a/mango/oracles/market/market.py b/mango/oracles/market/market.py index 90296cb..37bf9df 100644 --- a/mango/oracles/market/market.py +++ b/mango/oracles/market/market.py @@ -18,10 +18,10 @@ import rx import rx.operators as ops import typing -from datetime import datetime from decimal import Decimal from ...context import Context +from ...datetimes import utc_now from ...ensuremarketloaded import ensure_market_loaded from ...loadedmarket import LoadedMarket from ...market import Market @@ -86,7 +86,7 @@ class MarketOracle(Oracle): return Price( self.source, - datetime.now(), + utc_now(), self.market, top_bid, mid_price, diff --git a/mango/oracles/pythnetwork/pythnetwork.py b/mango/oracles/pythnetwork/pythnetwork.py index 64cf9a7..b6f924c 100644 --- a/mango/oracles/pythnetwork/pythnetwork.py +++ b/mango/oracles/pythnetwork/pythnetwork.py @@ -19,12 +19,12 @@ import rx import rx.operators as ops import typing -from datetime import datetime from decimal import Decimal from solana.publickey import PublicKey from ...accountinfo import AccountInfo from ...context import Context +from ...datetimes import utc_now from ...market import Market from ...observables import observable_pipeline_error_reporter from ...oracle import ( @@ -110,7 +110,13 @@ class PythOracle(Oracle): # Pyth has no notion of bids, asks, or spreads so just provide the single price. return Price( - self.source, datetime.now(), self.market, price, price, price, confidence + self.source, + utc_now(), + self.market, + price, + price, + price, + confidence, ) def to_streaming_observable( diff --git a/mango/oracles/stub/stub.py b/mango/oracles/stub/stub.py index b49c485..5c98a20 100644 --- a/mango/oracles/stub/stub.py +++ b/mango/oracles/stub/stub.py @@ -18,12 +18,12 @@ import rx import rx.operators as ops import typing -from datetime import datetime from decimal import Decimal from solana.publickey import PublicKey from ...cache import Cache from ...context import Context +from ...datetimes import utc_now from ...ensuremarketloaded import ensure_market_loaded from ...market import Market from ...observables import observable_pipeline_error_reporter @@ -83,7 +83,7 @@ class StubOracle(Oracle): # will give you the consistent results, but you'll need to adjust your code" return Price( self.source, - datetime.now(), + utc_now(), self.market, raw_price.price, raw_price.price, diff --git a/mango/orderbookside.py b/mango/orderbookside.py index d32059b..3f2dd66 100644 --- a/mango/orderbookside.py +++ b/mango/orderbookside.py @@ -16,7 +16,7 @@ import enum import typing -from datetime import datetime, timedelta, timezone +from datetime import timedelta, timezone from decimal import Decimal from solana.publickey import PublicKey @@ -141,51 +141,47 @@ class PerpOrderBookSide(AddressableAccount): stack = [self.root_node] orders: typing.List[Order] = [] - now: datetime = datetime.utcnow().astimezone(timezone.utc) while len(stack) > 0: index = int(stack.pop()) node = self.nodes[index] if node.type_name == "leaf": - expiration = ( - node.timestamp.astimezone(timezone.utc) - + timedelta(seconds=float(node.time_in_force)) - if node.time_in_force == 0 - else Order.NoExpiration + expiration = Order.NoExpiration + if node.time_in_force != 0: + expiration = node.timestamp.astimezone(timezone.utc) + timedelta( + seconds=float(node.time_in_force) + ) + + price = node.key["price"] + quantity = node.quantity + + decimals_differential = ( + self.perp_market_details.base_instrument.decimals + - self.perp_market_details.quote_token.token.decimals ) - if (node.time_in_force == 0) or (expiration > now): - price = node.key["price"] - quantity = node.quantity + native_to_ui = Decimal(10) ** decimals_differential + quote_lot_size = self.perp_market_details.quote_lot_size + base_lot_size = self.perp_market_details.base_lot_size + actual_price = price * (quote_lot_size / base_lot_size) * native_to_ui - decimals_differential = ( - self.perp_market_details.base_instrument.decimals - - self.perp_market_details.quote_token.token.decimals - ) - native_to_ui = Decimal(10) ** decimals_differential - quote_lot_size = self.perp_market_details.quote_lot_size - base_lot_size = self.perp_market_details.base_lot_size - actual_price = ( - price * (quote_lot_size / base_lot_size) * native_to_ui - ) + base_factor = ( + Decimal(10) ** self.perp_market_details.base_instrument.decimals + ) + actual_quantity = ( + quantity * self.perp_market_details.base_lot_size + ) / base_factor - base_factor = ( - Decimal(10) ** self.perp_market_details.base_instrument.decimals + orders += [ + Order( + int(node.key["order_id"]), + node.client_order_id, + node.owner, + order_side, + actual_price, + actual_quantity, + OrderType.UNKNOWN, + expiration=expiration, ) - actual_quantity = ( - quantity * self.perp_market_details.base_lot_size - ) / base_factor - - orders += [ - Order( - int(node.key["order_id"]), - node.client_order_id, - node.owner, - order_side, - actual_price, - actual_quantity, - OrderType.UNKNOWN, - expiration=expiration, - ) - ] + ] elif node.type_name == "inner": if order_side == Side.BUY: stack = [*stack, node.children[0], node.children[1]] diff --git a/mango/orders.py b/mango/orders.py index 5420a08..c7affe5 100644 --- a/mango/orders.py +++ b/mango/orders.py @@ -26,6 +26,7 @@ from pyserum.market.types import Order as PySerumOrder from solana.publickey import PublicKey from .constants import SYSTEM_PROGRAM_ADDRESS +from .datetimes import utc_now from .lotsizeconverter import LotSizeConverter @@ -170,6 +171,12 @@ class Order: high_order = id_bytes[8:] return Decimal(int.from_bytes(high_order, "little", signed=False)) + @property + def expired(self) -> bool: + if (self.expiration == Order.NoExpiration) or (self.expiration > utc_now()): + return False + return True + # Returns an identical order with the ID changed. def with_id(self, id: int) -> "Order": return Order( @@ -388,7 +395,7 @@ class Order: if expire_seconds is None or expire_seconds <= Decimal(0): return Order.NoExpiration - return datetime.now() + timedelta(seconds=float(expire_seconds)) + return utc_now() + timedelta(seconds=float(expire_seconds)) def __str__(self) -> str: owner: str = "" @@ -397,7 +404,7 @@ class Order: order_type: str = "" if self.order_type != OrderType.UNKNOWN: order_type = f" {self.order_type}" - return f"« Order {owner}{self.side} for {self.quantity:,.8f} at {self.price:.8f} [ID: {self.id} / {self.client_id}]{order_type}{' reduceOnly' if self.reduce_only else ''}»" + return f"« Order {owner}{self.side} for {self.quantity:,.8f} at {self.price:.8f} [ID: {self.id} / {self.client_id}]{order_type}{' reduceOnly' if self.reduce_only else ''}{' EXPIRED' if self.expired else ''} »" def __repr__(self) -> str: return f"{self}" @@ -420,7 +427,7 @@ class OrderBook: @property def bids(self) -> typing.Sequence[Order]: - return self.__bids + return list([o for o in self.__bids if not o.expired]) @bids.setter def bids(self, bids: typing.Sequence[Order]) -> None: @@ -431,7 +438,7 @@ class OrderBook: @property def asks(self) -> typing.Sequence[Order]: - return self.__asks + return list([o for o in self.__asks if not o.expired]) @asks.setter def asks(self, asks: typing.Sequence[Order]) -> None: @@ -443,28 +450,32 @@ class OrderBook: # The top bid is the highest price someone is willing to pay to BUY @property def top_bid(self) -> typing.Optional[Order]: - if self.bids and len(self.bids) > 0: + bids = self.bids + if bids and len(bids) > 0: # Top-of-book is always at index 0 for us. - return self.bids[0] + return bids[0] return None # The top ask is the lowest price someone is willing to pay to SELL @property def top_ask(self) -> typing.Optional[Order]: - if self.asks and len(self.asks) > 0: + asks = self.asks + if asks and len(asks) > 0: # Top-of-book is always at index 0 for us. - return self.asks[0] + return asks[0] return None # The mid price is halfway between the best bid and best ask. @property def mid_price(self) -> typing.Optional[Decimal]: - if self.top_bid is not None and self.top_ask is not None: - return (self.top_bid.price + self.top_ask.price) / 2 - elif self.top_bid is not None: - return self.top_bid.price - elif self.top_ask is not None: - return self.top_ask.price + top_bid = self.top_bid + top_ask = self.top_ask + if top_bid is not None and top_ask is not None: + return (top_bid.price + top_ask.price) / 2 + elif top_bid is not None: + return top_bid.price + elif top_ask is not None: + return top_ask.price return None @property @@ -476,8 +487,17 @@ class OrderBook: else: return top_ask.price - top_bid.price - def all_orders_for_owner(self, owner_address: PublicKey) -> typing.Sequence[Order]: - return list([o for o in [*self.bids, *self.asks] if o.owner == owner_address]) + def all_orders(self, include_expired: bool = False) -> typing.Sequence[Order]: + if include_expired: + return [*self.__bids, *self.__asks] + return [*self.bids, *self.asks] + + def all_orders_for_owner( + self, owner_address: PublicKey, include_expired: bool = False + ) -> typing.Sequence[Order]: + return list( + [o for o in self.all_orders(include_expired) if o.owner == owner_address] + ) def to_dataframe(self) -> pandas.DataFrame: column_mapper = { @@ -487,6 +507,7 @@ class OrderBook: "side": "Side", "price": "Price", "quantity": "Quantity", + "expiration": "Expiration", } frame: pandas.DataFrame = pandas.DataFrame([*reversed(self.bids), *self.asks]) diff --git a/mango/perpmarketdetails.py b/mango/perpmarketdetails.py index 0013315..2fb2972 100644 --- a/mango/perpmarketdetails.py +++ b/mango/perpmarketdetails.py @@ -15,13 +15,14 @@ import typing -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta from decimal import Decimal from solana.publickey import PublicKey from .accountinfo import AccountInfo from .addressableaccount import AddressableAccount from .context import Context +from .datetimes import utc_now from .group import GroupSlot, Group from .instrumentvalue import InstrumentValue from .layouts import layouts @@ -87,7 +88,7 @@ class LiquidityMiningInfo: # portion_given = 1 - mngoLeft / mngoPerPeriod # elapsed = ( - periodStart) / targetPeriodLength # est_next = elapsed / portion_given - elapsed - now: datetime = datetime.now().replace(microsecond=0).astimezone(timezone.utc) + now: datetime = utc_now().replace(microsecond=0) mngo_distributed: InstrumentValue = self.mngo_per_period - self.mngo_left proportion_distributed: Decimal = Decimal(0) elapsed: timedelta = now - self.period_start diff --git a/mango/perpmarketoperations.py b/mango/perpmarketoperations.py index 7c42c62..f5eeace 100644 --- a/mango/perpmarketoperations.py +++ b/mango/perpmarketoperations.py @@ -264,9 +264,11 @@ class PerpMarketOperations(MarketOperations): def load_orderbook(self) -> OrderBook: return self.perp_market.fetch_orderbook(self.context) - def load_my_orders(self) -> typing.Sequence[Order]: + def load_my_orders(self, include_expired: bool = False) -> typing.Sequence[Order]: orderbook: OrderBook = self.load_orderbook() - return orderbook.all_orders_for_owner(self.account.address) + return orderbook.all_orders_for_owner( + self.account.address, include_expired=include_expired + ) def _build_crank( self, limit: Decimal = Decimal(32), add_self: bool = False diff --git a/mango/reconnectingwebsocket.py b/mango/reconnectingwebsocket.py index f40a658..3e6682a 100644 --- a/mango/reconnectingwebsocket.py +++ b/mango/reconnectingwebsocket.py @@ -21,9 +21,10 @@ import rx.subject import typing import websocket -from datetime import datetime from threading import Thread +from .datetimes import local_now + # # 🥭 ReconnectingWebsocket class # @@ -41,16 +42,16 @@ class ReconnectingWebsocket: self.reconnect_required: bool = True self.ping_interval: int = 0 self.connecting: rx.subject.behaviorsubject.BehaviorSubject = ( - rx.subject.behaviorsubject.BehaviorSubject(datetime.now()) + rx.subject.behaviorsubject.BehaviorSubject(local_now()) ) self.disconnected: rx.subject.behaviorsubject.BehaviorSubject = ( - rx.subject.behaviorsubject.BehaviorSubject(datetime.now()) + rx.subject.behaviorsubject.BehaviorSubject(local_now()) ) self.ping: rx.subject.behaviorsubject.BehaviorSubject = ( - rx.subject.behaviorsubject.BehaviorSubject(datetime.now()) + rx.subject.behaviorsubject.BehaviorSubject(local_now()) ) self.pong: rx.subject.behaviorsubject.BehaviorSubject = ( - rx.subject.behaviorsubject.BehaviorSubject(datetime.now()) + rx.subject.behaviorsubject.BehaviorSubject(local_now()) ) self.item: rx.subject.subject.Subject = rx.subject.subject.Subject() @@ -76,10 +77,10 @@ class ReconnectingWebsocket: self._logger.warning(f"WebSocket for {self.url} has error {args}") def _on_ping(self, *_: typing.Any) -> None: - self.ping.on_next(datetime.now()) + self.ping.on_next(local_now()) def _on_pong(self, *_: typing.Any) -> None: - self.pong.on_next(datetime.now()) + self.pong.on_next(local_now()) def open(self) -> None: thread = Thread(target=self._run) @@ -91,7 +92,7 @@ class ReconnectingWebsocket: def _run(self) -> None: while self.reconnect_required: self._logger.info(f"WebSocket connecting to: {self.url}") - self.connecting.on_next(datetime.now()) + self.connecting.on_next(local_now()) self._ws = websocket.WebSocketApp( self.url, on_open=self._on_open, @@ -101,4 +102,4 @@ class ReconnectingWebsocket: on_pong=self._on_pong, ) self._ws.run_forever(ping_interval=self.ping_interval) - self.disconnected.on_next(datetime.now()) + self.disconnected.on_next(local_now()) diff --git a/mango/serummarketoperations.py b/mango/serummarketoperations.py index 74810eb..a709cb2 100644 --- a/mango/serummarketoperations.py +++ b/mango/serummarketoperations.py @@ -357,13 +357,15 @@ class SerumMarketOperations(MarketOperations): def load_orderbook(self) -> OrderBook: return self.serum_market.fetch_orderbook(self.context) - def load_my_orders(self) -> typing.Sequence[Order]: + def load_my_orders(self, include_expired: bool = False) -> typing.Sequence[Order]: open_orders_address = self.market_instruction_builder.open_orders_address if not open_orders_address: return [] orderbook: OrderBook = self.load_orderbook() - return orderbook.all_orders_for_owner(open_orders_address) + return orderbook.all_orders_for_owner( + open_orders_address, include_expired=include_expired + ) def _build_crank( self, limit: Decimal = Decimal(32), add_self: bool = False diff --git a/mango/simplemarketmaking/simplemarketmaker.py b/mango/simplemarketmaking/simplemarketmaker.py index b366fce..ff6572f 100644 --- a/mango/simplemarketmaking/simplemarketmaker.py +++ b/mango/simplemarketmaking/simplemarketmaker.py @@ -99,7 +99,9 @@ class SimpleMarketMaker: price, inventory ) - current_orders = self.market_operations.load_my_orders() + current_orders = self.market_operations.load_my_orders( + include_expired=True + ) buy_orders = [ order for order in current_orders if order.side == mango.Side.BUY ] @@ -145,7 +147,7 @@ class SimpleMarketMaker: def cleanup(self) -> None: self._logger.info("Cleaning up.") - orders = self.market_operations.load_my_orders() + orders = self.market_operations.load_my_orders(include_expired=True) for order in orders: self.market_operations.cancel_order(order) @@ -228,9 +230,14 @@ class SimpleMarketMaker: return len(orders) == 0 or not all( [ - (within_tolerance(price, order.price, self.existing_order_tolerance)) - and within_tolerance( - quantity, order.quantity, self.existing_order_tolerance + ( + not order.expired + and within_tolerance( + price, order.price, self.existing_order_tolerance + ) + and within_tolerance( + quantity, order.quantity, self.existing_order_tolerance + ) ) for order in orders ] diff --git a/mango/spotmarketoperations.py b/mango/spotmarketoperations.py index 03d672a..080ff35 100644 --- a/mango/spotmarketoperations.py +++ b/mango/spotmarketoperations.py @@ -355,12 +355,14 @@ class SpotMarketOperations(MarketOperations): def load_orderbook(self) -> OrderBook: return self.spot_market.fetch_orderbook(self.context) - def load_my_orders(self) -> typing.Sequence[Order]: + def load_my_orders(self, include_expired: bool = False) -> typing.Sequence[Order]: if not self.open_orders_address: return [] orderbook: OrderBook = self.load_orderbook() - return orderbook.all_orders_for_owner(self.open_orders_address) + return orderbook.all_orders_for_owner( + self.open_orders_address, include_expired=include_expired + ) def _build_crank( self, limit: Decimal = Decimal(32), add_self: bool = False diff --git a/mango/websocketsubscription.py b/mango/websocketsubscription.py index c339422..a14c732 100644 --- a/mango/websocketsubscription.py +++ b/mango/websocketsubscription.py @@ -18,7 +18,6 @@ import logging import typing import websocket -from datetime import datetime from rx.subject.behaviorsubject import BehaviorSubject from rx.core.typing import Disposable from solana.publickey import PublicKey @@ -26,6 +25,7 @@ from solana.rpc.types import RPCResponse from .accountinfo import AccountInfo from .context import Context +from .datetimes import local_now from .observables import EventSource from .reconnectingwebsocket import ReconnectingWebsocket @@ -61,7 +61,7 @@ class WebSocketSubscription( TSubscriptionInstance ]() self.ws: typing.Optional[ReconnectingWebsocket] = None - self.pong: BehaviorSubject = BehaviorSubject(datetime.now()) + self.pong: BehaviorSubject = BehaviorSubject(local_now()) self._pong_subscription: typing.Optional[Disposable] = None @abc.abstractmethod @@ -309,7 +309,7 @@ class SharedWebSocketSubscriptionManager(WebSocketSubscriptionManager): def __init__(self, context: Context, ping_interval: int = 10) -> None: super().__init__(context, ping_interval) self.ws: typing.Optional[ReconnectingWebsocket] = None - self.pong: BehaviorSubject = BehaviorSubject(datetime.now()) + self.pong: BehaviorSubject = BehaviorSubject(local_now()) self._pong_subscription: typing.Optional[Disposable] = None def open(self) -> None: diff --git a/tests/fakes.py b/tests/fakes.py index f9f531b..f71880b 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -1,11 +1,9 @@ import construct -import datetime import mango import mango.marketmaking import typing from decimal import Decimal -from mango.lotsizeconverter import NullLotSizeConverter from pyserum.market.market import Market as PySerumMarket from pyserum.market.state import MarketState as PySerumMarketState from solana.keypair import Keypair @@ -250,7 +248,7 @@ def fake_price( mango.OracleSource( "test", "test", mango.SupportedOracleFeature.TOP_BID_AND_OFFER, market ), - datetime.datetime.now(), + mango.utc_now(), market, bid, price, @@ -337,7 +335,7 @@ def fake_root_bank() -> mango.RootBank: [], Decimal(0), Decimal(0), - datetime.datetime.now(), + mango.utc_now(), ) @@ -347,7 +345,11 @@ def fake_cache() -> mango.Cache: def fake_root_bank_cache() -> mango.RootBankCache: - return mango.RootBankCache(Decimal(1), Decimal(2), datetime.datetime.now()) + return mango.RootBankCache( + Decimal(1), + Decimal(2), + mango.utc_now(), + ) def fake_group(address: typing.Optional[PublicKey] = None) -> mango.Group: @@ -467,7 +469,7 @@ def fake_model_state( placed_orders_container = placed_orders_container or fake_placed_orders_container() inventory = inventory or fake_inventory() orderbook = orderbook or mango.OrderBook( - "FAKE", NullLotSizeConverter(), fake_bids(), fake_asks() + "FAKE", mango.NullLotSizeConverter(), fake_bids(), fake_asks() ) event_queue = event_queue or mango.NullEventQueue() group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher( diff --git a/tests/marketmaking/orderchain/test_ratios.py b/tests/marketmaking/orderchain/test_ratios.py index e826a7d..376cd8d 100644 --- a/tests/marketmaking/orderchain/test_ratios.py +++ b/tests/marketmaking/orderchain/test_ratios.py @@ -3,7 +3,7 @@ import argparse from ...context import mango from ...fakes import fake_context, fake_model_state, fake_price -from datetime import datetime, timedelta +from datetime import timedelta from decimal import Decimal from mango.marketmaking.orderchain.ratioselement import RatiosElement @@ -45,8 +45,8 @@ def test_uses_specified_order_parameters() -> None: ) result = actual.process(context, model_state, []) - assert result[0].expiration > datetime.now() - timedelta(seconds=1) - assert result[0].expiration < datetime.now() + timedelta(seconds=5) + assert result[0].expiration > mango.utc_now() - timedelta(seconds=1) + assert result[0].expiration < mango.utc_now() + timedelta(seconds=5) assert result[0].match_limit == 15 assert result[0].order_type == mango.OrderType.POST_ONLY_SLIDE diff --git a/tests/marketmaking/test_toleranceorderreconciler.py b/tests/marketmaking/test_toleranceorderreconciler.py index 66ef79b..66889b5 100644 --- a/tests/marketmaking/test_toleranceorderreconciler.py +++ b/tests/marketmaking/test_toleranceorderreconciler.py @@ -1,6 +1,6 @@ import mango -from datetime import datetime, timedelta +from datetime import timedelta from decimal import Decimal from mango.marketmaking.toleranceorderreconciler import ToleranceOrderReconciler @@ -214,7 +214,7 @@ def test_price_outside_negative_tolerance_no_match() -> None: def test_time_in_force_early_match() -> None: - now = datetime.now() + now = mango.utc_now() actual = ToleranceOrderReconciler( Decimal("0.001"), Decimal(0), timedelta(seconds=5) ) @@ -232,7 +232,7 @@ def test_time_in_force_early_match() -> None: def test_time_in_force_too_early_no_match() -> None: - now = datetime.now() + now = mango.utc_now() actual = ToleranceOrderReconciler( Decimal("0.001"), Decimal(0), timedelta(seconds=5) ) @@ -250,7 +250,7 @@ def test_time_in_force_too_early_no_match() -> None: def test_time_in_force_late_match() -> None: - now = datetime.now() + now = mango.utc_now() actual = ToleranceOrderReconciler( Decimal("0.001"), Decimal(0), timedelta(seconds=5) ) @@ -268,7 +268,7 @@ def test_time_in_force_late_match() -> None: def test_time_in_force_too_late_no_match() -> None: - now = datetime.now() + now = mango.utc_now() actual = ToleranceOrderReconciler( Decimal("0.001"), Decimal(0), timedelta(seconds=5) ) diff --git a/tests/test_cache.py b/tests/test_cache.py index 5b8309a..5130f79 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -4,7 +4,6 @@ from .context import mango from .fakes import fake_account_info, fake_seeded_public_key from .data import load_cache -from datetime import datetime from decimal import Decimal @@ -13,7 +12,7 @@ def test_cache_constructor() -> None: meta_data = mango.Metadata( mango.layouts.DATA_TYPE.parse(bytearray(b"\x07")), mango.Version.V1, True ) - timestamp = datetime.now() + timestamp = mango.utc_now() price_cache = [mango.PriceCache(Decimal(26), timestamp)] root_bank_cache = [ mango.RootBankCache(Decimal("0.00001"), Decimal("0.00001"), timestamp) diff --git a/tests/test_order.py b/tests/test_order.py new file mode 100644 index 0000000..07ce072 --- /dev/null +++ b/tests/test_order.py @@ -0,0 +1,28 @@ +from .context import mango + +from datetime import timedelta +from decimal import Decimal + + +def test_order_expired() -> None: + actual = mango.Order.from_basic_info( + side=mango.Side.BUY, + price=Decimal(10), + quantity=Decimal(20), + order_type=mango.OrderType.LIMIT, + expiration=mango.utc_now() - timedelta(seconds=1), + ) + + assert actual.expired + + +def test_order_not_expired() -> None: + actual = mango.Order.from_basic_info( + side=mango.Side.BUY, + price=Decimal(10), + quantity=Decimal(20), + order_type=mango.OrderType.LIMIT, + expiration=mango.utc_now() + timedelta(seconds=5), + ) + + assert not actual.expired diff --git a/tests/test_perpeventqueue.py b/tests/test_perpeventqueue.py index 50c54af..44e04d9 100644 --- a/tests/test_perpeventqueue.py +++ b/tests/test_perpeventqueue.py @@ -5,7 +5,6 @@ from solana.publickey import PublicKey from .context import mango from .fakes import fake_account_info, fake_seeded_public_key -from datetime import datetime from decimal import Decimal @@ -85,7 +84,7 @@ class TstFillPE(mango.PerpFillEvent): super().__init__( 0, Decimal(0), - datetime.now(), + mango.utc_now(), mango.Side.BUY, Decimal(1), Decimal(1), diff --git a/tests/test_tokenbank.py b/tests/test_tokenbank.py index 6287ee9..13bf1f8 100644 --- a/tests/test_tokenbank.py +++ b/tests/test_tokenbank.py @@ -39,7 +39,7 @@ def test_root_bank_constructor() -> None: node_bank = fake_seeded_public_key("node bank") deposit_index = Decimal(98765) borrow_index = Decimal(12345) - timestamp = datetime.now() + timestamp = mango.utc_now() actual = mango.RootBank( account_info, diff --git a/tests/test_transactionscout.py b/tests/test_transactionscout.py index 93828cf..fdc47bd 100644 --- a/tests/test_transactionscout.py +++ b/tests/test_transactionscout.py @@ -26,7 +26,7 @@ def test_transaction_instruction_constructor() -> None: def test_transaction_scout_constructor() -> None: - timestamp: datetime = datetime.now() + timestamp: datetime = mango.utc_now() signatures: typing.Sequence[str] = ["Signature1", "Signature2"] succeeded: bool = True group_name: str = "BTC_ETH_USDT"