OrderBook can now provide expired orders, if requested.

This commit is contained in:
Geoff Taylor 2022-02-22 12:06:20 +00:00
parent c3f7ad60f6
commit 906b419fc6
36 changed files with 251 additions and 137 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

37
mango/datetimes.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = (<current_time> - 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

28
tests/test_order.py Normal file
View File

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

View File

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

View File

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

View File

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