More realistic market-maker is now available.

This commit is contained in:
Geoff Taylor 2021-07-15 21:03:22 +01:00
parent 9960713d7d
commit 7db8326a7c
27 changed files with 858 additions and 131 deletions

View File

@ -6,8 +6,6 @@ import os
import os.path import os.path
import sys import sys
from decimal import Decimal
sys.path.insert(0, os.path.abspath( sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), ".."))) os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8 import mango # nopep8
@ -32,7 +30,6 @@ if market is None:
raise Exception(f"Could not find market {market_symbol}") raise Exception(f"Could not find market {market_symbol}")
market_operations = mango.create_market_operations(context, wallet, False, market) market_operations = mango.create_market_operations(context, wallet, False, market)
order = mango.Order(id=args.order_id, client_id=0, owner=wallet.address, order = mango.Order.from_ids(id=args.order_id, client_id=0)
side=mango.Side.BUY, price=Decimal(0), size=Decimal(0))
cancellation = market_operations.cancel_order(order) cancellation = market_operations.cancel_order(order)
print(cancellation) print(cancellation)

193
bin/market-maker Executable file
View File

@ -0,0 +1,193 @@
#!/usr/bin/env pyston3
import argparse
import logging
import os
import os.path
import rx
import sys
import typing
from decimal import Decimal
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
import mango.marketmaking.fixedratiosdesiredordersbuilder # nopep8
import mango.marketmaking.marketmaker # nopep8
import mango.marketmaking.modelstate # nopep8
import mango.marketmaking.toleranceorderreconciler # nopep8
parser = argparse.ArgumentParser(description="Shows the on-chain data of a particular account.")
mango.Context.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol to make market upon (e.g. ETH/USDC)")
parser.add_argument("--account-index", type=int, default=0,
help="index of the account to use, if more than one available")
parser.add_argument("--oracle-provider", type=str, required=True,
help="name of the price provider to use (e.g. pyth)")
parser.add_argument("--spread-ratio", type=Decimal, required=True,
help="fraction of the mid price to be added and subtracted to calculate buy and sell prices")
parser.add_argument("--position-size-ratio", type=Decimal, required=True,
help="fraction of the token inventory to be bought or sold in each order")
parser.add_argument("--existing-order-tolerance", type=Decimal, default=Decimal("0.001"),
help="tolerance in price and quantity when matching existing orders or cancelling/replacing")
parser.add_argument("--dry-run", action="store_true", default=False,
help="runs as read-only and does not perform any transactions")
args = parser.parse_args()
logging.getLogger().setLevel(args.log_level)
logging.warning(mango.WARNING_DISCLAIMER_TEXT)
def cleanup(context: mango.Context, wallet: mango.Wallet, dry_run: bool, market: mango.Market):
logging.info("Cleaning up.")
market_operations: mango.MarketOperations = mango.create_market_operations(context, wallet, dry_run, market)
orders = market_operations.load_my_orders()
for order in orders:
market_operations.cancel_order(order)
def add_file_health(name: str, event_source: mango.EventSource, disposer: mango.DisposePropagator):
perp_market_file_touch_disposer = event_source.subscribe(
mango.FileToucherObserver(f"/var/tmp/mango_healthcheck_{name}"))
disposer.add_disposable(perp_market_file_touch_disposer)
def build_latest_group_observer(context: mango.Context, manager: mango.WebSocketSubscriptionManager, disposer: mango.DisposePropagator, group: mango.Group) -> mango.LatestItemObserverSubscriber[mango.Group]:
group_subscription = mango.WebSocketSubscription[mango.Group](
context, group.address, lambda account_info: mango.Group.parse(context, account_info))
manager.add(group_subscription)
latest_group_observer = mango.LatestItemObserverSubscriber(group)
group_subscription.publisher.subscribe(latest_group_observer)
add_file_health("group_subscription", group_subscription.publisher, disposer)
return latest_group_observer
def build_latest_account_observer(context: mango.Context, account: mango.Account, manager: mango.WebSocketSubscriptionManager, disposer: mango.DisposePropagator, group_observer: mango.LatestItemObserverSubscriber[mango.Group]) -> mango.LatestItemObserverSubscriber[mango.Account]:
account_subscription = mango.WebSocketSubscription[mango.Account](
context, account.address, lambda account_info: mango.Account.parse(context, account_info, group_observer.latest))
manager.add(account_subscription)
latest_account_observer = mango.LatestItemObserverSubscriber(account)
account_subscription.publisher.subscribe(latest_account_observer)
add_file_health("account_subscription", account_subscription.publisher, disposer)
return latest_account_observer
def build_latest_spot_open_orders_observer(manager: mango.WebSocketSubscriptionManager, disposer: mango.DisposePropagator, spot_market: mango.SpotMarket) -> mango.LatestItemObserverSubscriber[mango.OpenOrders]:
market_index = group.find_spot_market_index(spot_market.address)
spot_open_orders_address = account.spot_open_orders[market_index]
spot_open_orders_subscription = mango.WebSocketSubscription[mango.OpenOrders](
context, spot_open_orders_address, lambda account_info: mango.OpenOrders.parse(account_info, spot_market.base.decimals, spot_market.quote.decimals))
manager.add(spot_open_orders_subscription)
initial_spot_open_orders = mango.OpenOrders.load(
context, spot_open_orders_address, spot_market.base.decimals, spot_market.quote.decimals)
latest_spot_open_orders_observer = mango.LatestItemObserverSubscriber(initial_spot_open_orders)
spot_open_orders_subscription.publisher.subscribe(latest_spot_open_orders_observer)
add_file_health("spot_open_orders_subscription", spot_open_orders_subscription.publisher, disposer)
return latest_spot_open_orders_observer
def build_latest_perp_market_observer(manager: mango.WebSocketSubscriptionManager, disposer: mango.DisposePropagator, perp_market_info: mango.PerpMarketInfo, initial_perp_market: mango.PerpMarket, group_observer: mango.LatestItemObserverSubscriber[mango.Group]) -> mango.LatestItemObserverSubscriber[mango.PerpMarket]:
perp_market_subscription = mango.WebSocketSubscription[mango.PerpMarket](
context, perp_market_info.address, lambda account_info: mango.PerpMarket.parse(account_info, group_observer.latest))
manager.add(perp_market_subscription)
latest_perp_market_observer = mango.LatestItemObserverSubscriber(initial_perp_market)
perp_market_subscription.publisher.subscribe(latest_perp_market_observer)
add_file_health("perp_market_subscription", perp_market_subscription.publisher, disposer)
return latest_perp_market_observer
def build_latest_price_observer(context: mango.Context, disposer: mango.DisposePropagator, provider_name: str, market: mango.Market) -> mango.LatestItemObserverSubscriber[mango.Price]:
oracle_provider: mango.OracleProvider = mango.create_oracle_provider(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}.")
initial_price = oracle.fetch_price(context)
price_feed = oracle.to_streaming_observable(context)
latest_price_observer = mango.LatestItemObserverSubscriber(initial_price)
price_disposable = price_feed.subscribe(latest_price_observer)
disposer.add_disposable(price_disposable)
add_file_health("price_subscription", price_feed, disposer)
return latest_price_observer
context = mango.Context.from_command_line_parameters(args)
disposer = mango.DisposePropagator()
manager = mango.WebSocketSubscriptionManager()
disposer.add_disposable(manager)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_id)
accounts = mango.Account.load_all_for_owner(context, wallet.address, group)
if len(accounts) == 0:
raise Exception(f"No mango account found for root address '{wallet.address}'.")
account = accounts[args.account_index]
market_symbol = args.market.upper()
market = context.market_lookup.find_by_symbol(market_symbol)
if market is None:
raise Exception(f"Could not find market {market_symbol}")
# The market index is also the index of the base token in the group's token list.
if market.quote != group.shared_quote_token.token:
raise Exception(
f"Group {group.name} uses quote token {group.shared_quote_token.token.symbol}, not use shared quote token {market.quote.symbol}.")
cleanup(context, wallet, args.dry_run, market)
latest_group_observer = build_latest_group_observer(context, manager, disposer, group)
latest_account_observer = build_latest_account_observer(context, account, manager, disposer, latest_group_observer)
latest_price_observer = build_latest_price_observer(context, disposer, args.oracle_provider, market)
market_instruction_builder: typing.Optional[mango.MarketInstructionBuilder] = None
if isinstance(market, mango.SerumMarket):
market_instruction_builder = mango.SerumMarketInstructionBuilder.load(context, wallet, market)
elif isinstance(market, mango.SpotMarket):
market_as_spot_market: mango.SpotMarket = typing.cast(mango.SpotMarket, market)
market_instruction_builder = mango.SpotMarketInstructionBuilder.load(context, wallet, group, account, market)
latest_perp_market_observer = None
latest_spot_open_orders_observer = build_latest_spot_open_orders_observer(manager, disposer, market_as_spot_market)
elif isinstance(market, mango.PerpsMarket):
perp_market_info = mango.PerpMarketInfo.find_by_address(group.perp_markets, market.address)
initial_perp_market = mango.PerpMarket.load(context, perp_market_info.address, group)
latest_perp_market_observer = build_latest_perp_market_observer(
manager, disposer, perp_market_info, initial_perp_market, latest_group_observer)
market_instruction_builder = mango.PerpMarketInstructionBuilder.load(
context, wallet, group, account, initial_perp_market)
else:
raise Exception(f"Could not determine type of market {market.symbol}")
websocket_url = context.cluster_url.replace("https", "ws", 1)
ws: mango.ReconnectingWebsocket = mango.ReconnectingWebsocket(websocket_url, manager.open_handler, manager.on_item)
ws.ping_interval = 10
ws.open()
order_reconciler = mango.marketmaking.toleranceorderreconciler.ToleranceOrderReconciler(
args.existing_order_tolerance, args.existing_order_tolerance)
desired_orders_builder = mango.marketmaking.fixedratiosdesiredordersbuilder.FixedRatiosDesiredOrdersBuilder(
[args.spread_ratio, args.spread_ratio * 2, args.spread_ratio * 3],
[args.position_size_ratio, args.position_size_ratio, args.position_size_ratio])
market_maker = mango.marketmaking.marketmaker.MarketMaker(
wallet, market, market_instruction_builder, desired_orders_builder, order_reconciler)
model_state = mango.marketmaking.modelstate.ModelState(market, latest_account_observer,
latest_group_observer, latest_price_observer,
latest_perp_market_observer,
latest_spot_open_orders_observer)
pulse_disposable = rx.interval(10).subscribe(on_next=lambda _: market_maker.pulse(context, model_state))
disposer.add_disposable(pulse_disposable)
print("Press <ENTER> to quit.")
# Wait - don't exit
input()
print("Shutting down...")
ws.close()
disposer.dispose()
cleanup(context, wallet, args.dry_run, market)
print("Done.")

View File

@ -24,7 +24,7 @@ from .marketlookup import MarketLookup, CompoundMarketLookup
from .marketoperations import MarketOperations, NullMarketOperations from .marketoperations import MarketOperations, NullMarketOperations
from .metadata import Metadata from .metadata import Metadata
from .notification import NotificationTarget, TelegramNotificationTarget, DiscordNotificationTarget, MailjetNotificationTarget, CsvFileNotificationTarget, FilteringNotificationTarget, NotificationHandler, parse_subscription_target from .notification import NotificationTarget, TelegramNotificationTarget, DiscordNotificationTarget, MailjetNotificationTarget, CsvFileNotificationTarget, FilteringNotificationTarget, NotificationHandler, parse_subscription_target
from .observables import DisposePropagator, NullObserverSubscriber, PrintingObserverSubscriber, TimestampedPrintingObserverSubscriber, CollectingObserverSubscriber, LatestItemObserverSubscriber, CaptureFirstItem, FunctionObserver, create_backpressure_skipping_observer, debug_print_item, log_subscription_error, observable_pipeline_error_reporter, EventSource from .observables import DisposePropagator, NullObserverSubscriber, PrintingObserverSubscriber, TimestampedPrintingObserverSubscriber, CollectingObserverSubscriber, LatestItemObserverSubscriber, CaptureFirstItem, FunctionObserver, create_backpressure_skipping_observer, debug_print_item, log_subscription_error, observable_pipeline_error_reporter, EventSource, FileToucherObserver
from .openorders import OpenOrders from .openorders import OpenOrders
from .orderbookside import OrderBookSide from .orderbookside import OrderBookSide
from .orders import Order, OrderType, Side from .orders import Order, OrderType, Side

View File

@ -91,7 +91,8 @@ class CombinableInstructions():
current_chunk: typing.List[TransactionInstruction] = [] current_chunk: typing.List[TransactionInstruction] = []
for instruction in self.instructions: for instruction in self.instructions:
in_progress_chunk = current_chunk + [instruction] in_progress_chunk = current_chunk + [instruction]
if CombinableInstructions.transaction_size(self.signers, in_progress_chunk) < _MAXIMUM_TRANSACTION_LENGTH: transaction_size = CombinableInstructions.transaction_size(self.signers, in_progress_chunk)
if transaction_size < _MAXIMUM_TRANSACTION_LENGTH:
current_chunk = in_progress_chunk current_chunk = in_progress_chunk
else: else:
vetted_chunks += [current_chunk] vetted_chunks += [current_chunk]
@ -99,9 +100,18 @@ class CombinableInstructions():
all_chunks = vetted_chunks + [current_chunk] all_chunks = vetted_chunks + [current_chunk]
if len(all_chunks) == 1 and len(all_chunks[0]) == 0:
self.logger.info("No instructions to run.")
return []
if len(all_chunks) > 1: if len(all_chunks) > 1:
self.logger.info(f"Running instructions in {len(all_chunks)} transactions.") self.logger.info(f"Running instructions in {len(all_chunks)} transactions.")
total_in_chunks = sum(map(lambda chunk: len(chunk), all_chunks))
if total_in_chunks != len(self.instructions):
raise Exception(
f"Failed to chunk instructions. Have {total_in_chunks} instuctions in chunks. Should have {len(self.instructions)}.")
results = [] results = []
for chunk in all_chunks: for chunk in all_chunks:
transaction = Transaction() transaction = Transaction()

View File

@ -50,7 +50,7 @@ from .wallet import Wallet
# to send to Solana. # to send to Solana.
# #
# One important distinction between these functions and the more common `create instruction functions` in # One important distinction between these functions and the more common `create instruction functions` in
# Solana is that these functions *all return a list of instructions and signers*. # Solana is that these functions *all return a combinable of instructions and signers*.
# #
# It's likely that some operations will require actions split across multiple instructions because of # It's likely that some operations will require actions split across multiple instructions because of
# instruction size limitiations, so all our functions are prepared for this without having to change # instruction size limitiations, so all our functions are prepared for this without having to change

View File

@ -20,7 +20,7 @@ import logging
from decimal import Decimal from decimal import Decimal
from .combinableinstructions import CombinableInstructions from .combinableinstructions import CombinableInstructions
from .orders import Order, OrderType, Side from .orders import Order
# # 🥭 MarketInstructionBuilder class # # 🥭 MarketInstructionBuilder class
@ -50,7 +50,7 @@ class MarketInstructionBuilder(metaclass=abc.ABCMeta):
"MarketInstructionBuilder.build_cancel_order_instructions() is not implemented on the base type.") "MarketInstructionBuilder.build_cancel_order_instructions() is not implemented on the base type.")
@abc.abstractmethod @abc.abstractmethod
def build_place_order_instructions(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal, client_order_id: int) -> CombinableInstructions: def build_place_order_instructions(self, order: Order) -> CombinableInstructions:
raise NotImplementedError( raise NotImplementedError(
"MarketInstructionBuilder.build_place_order_instructions() is not implemented on the base type.") "MarketInstructionBuilder.build_place_order_instructions() is not implemented on the base type.")
@ -81,7 +81,7 @@ class NullMarketInstructionBuilder(MarketInstructionBuilder):
def build_cancel_order_instructions(self, order: Order) -> CombinableInstructions: def build_cancel_order_instructions(self, order: Order) -> CombinableInstructions:
return CombinableInstructions.empty() return CombinableInstructions.empty()
def build_place_order_instructions(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal, client_order_id: int) -> CombinableInstructions: def build_place_order_instructions(self, order: Order) -> CombinableInstructions:
return CombinableInstructions.empty() return CombinableInstructions.empty()
def build_settle_instructions(self) -> CombinableInstructions: def build_settle_instructions(self) -> CombinableInstructions:

View File

@ -19,7 +19,6 @@ import logging
import mango import mango
import typing import typing
from .desiredorder import DesiredOrder
from .modelstate import ModelState from .modelstate import ModelState
@ -35,7 +34,7 @@ class DesiredOrdersBuilder(metaclass=abc.ABCMeta):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@abc.abstractmethod @abc.abstractmethod
def build(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[DesiredOrder]: def build(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[mango.Order]:
raise NotImplementedError("DesiredOrdersBuilder.build() is not implemented on the base type.") raise NotImplementedError("DesiredOrdersBuilder.build() is not implemented on the base type.")
def __repr__(self) -> str: def __repr__(self) -> str:
@ -51,7 +50,7 @@ class NullDesiredOrdersBuilder(DesiredOrdersBuilder):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
def build(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[DesiredOrder]: def build(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[mango.Order]:
return [] return []
def __str__(self) -> str: def __str__(self) -> str:

View File

@ -20,46 +20,52 @@ import typing
from decimal import Decimal from decimal import Decimal
from .desiredorder import DesiredOrder
from .desiredordersbuilder import DesiredOrdersBuilder from .desiredordersbuilder import DesiredOrdersBuilder
from .modelstate import ModelState from .modelstate import ModelState
# # 🥭 FixedRatioDesiredOrdersBuilder class # # 🥭 FixedRatiosDesiredOrdersBuilder class
# #
# Builds orders using a fixed spread ratio and a fixed position size ratio. # Builds orders using a fixed spread ratio and a fixed position size ratio.
# #
class FixedRatioDesiredOrdersBuilder(DesiredOrdersBuilder): class FixedRatiosDesiredOrdersBuilder(DesiredOrdersBuilder):
def __init__(self, spread_ratio: Decimal, position_size_ratio: Decimal): def __init__(self, spread_ratios: typing.Sequence[Decimal], position_size_ratios: typing.Sequence[Decimal]):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.spread_ratio: Decimal = spread_ratio if len(spread_ratios) != len(position_size_ratios):
self.position_size_ratio: Decimal = position_size_ratio raise Exception("List of spread ratios and position size ratios must be the same length.")
def build(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[DesiredOrder]: self.spread_ratios: typing.Sequence[Decimal] = spread_ratios
self.position_size_ratios: typing.Sequence[Decimal] = position_size_ratios
def build(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[mango.Order]:
price: mango.Price = model_state.price price: mango.Price = model_state.price
inventory: typing.Sequence[typing.Optional[mango.TokenValue]] = model_state.account.net_assets inventory: typing.Sequence[typing.Optional[mango.TokenValue]] = model_state.account.net_assets
base_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.base) base_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.base)
if base_tokens is None:
raise Exception(f"Could not find market-maker base token {price.market.base.symbol} in inventory.")
quote_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.quote) quote_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.quote)
if quote_tokens is None:
raise Exception(f"Could not find market-maker quote token {price.market.quote.symbol} in inventory.")
total = (base_tokens.value * price.mid_price) + quote_tokens.value total = (base_tokens.value * price.mid_price) + quote_tokens.value
position_size = total * self.position_size_ratio
buy_size: Decimal = position_size / price.mid_price orders: typing.List[mango.Order] = []
sell_size: Decimal = position_size / price.mid_price for counter in range(len(self.spread_ratios)):
position_size_ratio = self.position_size_ratios[counter]
spread_ratio = self.spread_ratios[counter]
bid: Decimal = price.mid_price - (price.mid_price * self.spread_ratio) position_size = total * position_size_ratio
ask: Decimal = price.mid_price + (price.mid_price * self.spread_ratio) buy_quantity: Decimal = position_size / price.mid_price
sell_quantity: Decimal = position_size / price.mid_price
return [ bid: Decimal = price.mid_price - (price.mid_price * spread_ratio)
DesiredOrder(mango.Side.BUY, mango.OrderType.POST_ONLY, bid, buy_size), ask: Decimal = price.mid_price + (price.mid_price * spread_ratio)
DesiredOrder(mango.Side.SELL, mango.OrderType.POST_ONLY, ask, sell_size)
] orders += [
mango.Order.from_basic_info(mango.Side.BUY, price=bid, quantity=buy_quantity,
order_type=mango.OrderType.POST_ONLY),
mango.Order.from_basic_info(mango.Side.SELL, price=ask, quantity=sell_quantity,
order_type=mango.OrderType.POST_ONLY)
]
return orders
def __str__(self) -> str: def __str__(self) -> str:
return f"« 𝙵𝚒𝚡𝚎𝚍𝚁𝚊𝚝𝚒𝚘𝙳𝚎𝚜𝚒𝚛𝚎𝚍𝙾𝚛𝚍𝚎𝚛𝚜𝙱𝚞𝚒𝚕𝚍𝚎𝚛 using ratios - spread: {self.spread_ratio}, position size: {self.position_size_ratio} »" return f"« 𝙵𝚒𝚡𝚎𝚍𝚁𝚊𝚝𝚒𝚘𝙳𝚎𝚜𝚒𝚛𝚎𝚍𝙾𝚛𝚍𝚎𝚛𝚜𝙱𝚞𝚒𝚕𝚍𝚎𝚛 using ratios - spread: {self.spread_ratio}, position size: {self.position_size_ratio} »"

View File

@ -19,10 +19,10 @@ import mango
import traceback import traceback
import typing import typing
from decimal import Decimal
from .desiredordersbuilder import DesiredOrdersBuilder from .desiredordersbuilder import DesiredOrdersBuilder
from .modelstate import ModelState from .modelstate import ModelState
from .orderreconciler import OrderReconciler
from .ordertracker import OrderTracker
# # 🥭 MarketMaker class # # 🥭 MarketMaker class
@ -33,48 +33,44 @@ from .modelstate import ModelState
class MarketMaker: class MarketMaker:
def __init__(self, wallet: mango.Wallet, market: mango.Market, def __init__(self, wallet: mango.Wallet, market: mango.Market,
market_instruction_builder: mango.MarketInstructionBuilder, market_instruction_builder: mango.MarketInstructionBuilder,
desired_orders_builder: DesiredOrdersBuilder): desired_orders_builder: DesiredOrdersBuilder,
order_reconciler: OrderReconciler):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.wallet: mango.Wallet = wallet self.wallet: mango.Wallet = wallet
self.market: mango.Market = market self.market: mango.Market = market
self.market_instruction_builder: mango.MarketInstructionBuilder = market_instruction_builder self.market_instruction_builder: mango.MarketInstructionBuilder = market_instruction_builder
self.desired_orders_builder: DesiredOrdersBuilder = desired_orders_builder self.desired_orders_builder: DesiredOrdersBuilder = desired_orders_builder
self.order_reconciler: OrderReconciler = order_reconciler
self.order_tracker: OrderTracker = OrderTracker()
self.buy_client_ids: typing.List[int] = [] self.buy_client_ids: typing.List[int] = []
self.sell_client_ids: typing.List[int] = [] self.sell_client_ids: typing.List[int] = []
def pulse(self, context: mango.Context, model_state: ModelState): def pulse(self, context: mango.Context, model_state: ModelState):
try: try:
desired_orders = self.desired_orders_builder.build(context, model_state)
payer = mango.CombinableInstructions.from_wallet(self.wallet) payer = mango.CombinableInstructions.from_wallet(self.wallet)
desired_orders = self.desired_orders_builder.build(context, model_state)
existing_orders = self.order_tracker.existing_orders(model_state)
reconciled = self.order_reconciler.reconcile(model_state, existing_orders, desired_orders)
cancellations = mango.CombinableInstructions.empty() cancellations = mango.CombinableInstructions.empty()
for order_id, client_id in model_state.placed_order_ids: for to_cancel in reconciled.to_cancel:
if client_id != 0: cancel = self.market_instruction_builder.build_cancel_order_instructions(to_cancel)
self.logger.info(f"Cancelling order with client ID: {client_id}") cancellations += cancel
side = mango.Side.BUY if client_id in self.buy_client_ids else mango.Side.SELL
order = mango.Order(id=int(order_id), client_id=int(client_id), owner=self.wallet.address,
side=side, price=Decimal(0), size=Decimal(0))
cancel = self.market_instruction_builder.build_cancel_order_instructions(order)
cancellations += cancel
place_orders = mango.CombinableInstructions.empty() place_orders = mango.CombinableInstructions.empty()
for desired_order in desired_orders: for to_place in reconciled.to_place:
desired_client_id: int = context.random_client_id() desired_client_id: int = context.random_client_id()
if desired_order.side == mango.Side.BUY: to_place_with_client_id = to_place.with_client_id(desired_client_id)
self.buy_client_ids += [desired_client_id] self.order_tracker.track(to_place_with_client_id)
else:
self.sell_client_ids += [desired_client_id]
self.logger.info( self.logger.info(
f"Placing {desired_order.side} order for {desired_order.quantity} at price {desired_order.price} with client ID: {desired_client_id}") f"Placing {to_place_with_client_id.side} order for {to_place_with_client_id.quantity} at price {to_place_with_client_id.price} with client ID: {to_place_with_client_id.client_id}")
place_order = self.market_instruction_builder.build_place_order_instructions( place_order = self.market_instruction_builder.build_place_order_instructions(to_place_with_client_id)
desired_order.side, desired_order.order_type, desired_order.price, desired_order.quantity, desired_client_id)
place_orders += place_order place_orders += place_order
settle = self.market_instruction_builder.build_settle_instructions() settle = self.market_instruction_builder.build_settle_instructions()
crank = self.market_instruction_builder.build_crank_instructions() crank = self.market_instruction_builder.build_crank_instructions()
(payer + cancellations + place_orders + settle + crank).execute(context) (payer + cancellations + place_orders + settle + crank).execute(context)
except Exception as exception: except Exception as exception:

View File

@ -32,7 +32,6 @@ class ModelState:
group_watcher: mango.LatestItemObserverSubscriber[mango.Group], group_watcher: mango.LatestItemObserverSubscriber[mango.Group],
price_watcher: mango.LatestItemObserverSubscriber[mango.Price], price_watcher: mango.LatestItemObserverSubscriber[mango.Price],
perp_market_watcher: typing.Optional[mango.LatestItemObserverSubscriber[mango.PerpMarket]], perp_market_watcher: typing.Optional[mango.LatestItemObserverSubscriber[mango.PerpMarket]],
spot_market_watcher: mango.LatestItemObserverSubscriber[mango.SpotMarket],
spot_open_orders_watcher: mango.LatestItemObserverSubscriber[mango.OpenOrders] spot_open_orders_watcher: mango.LatestItemObserverSubscriber[mango.OpenOrders]
): ):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@ -42,7 +41,6 @@ class ModelState:
self.price_watcher: mango.LatestItemObserverSubscriber[mango.Price] = price_watcher self.price_watcher: mango.LatestItemObserverSubscriber[mango.Price] = price_watcher
self.perp_market_watcher: typing.Optional[mango.LatestItemObserverSubscriber[mango.PerpMarket] self.perp_market_watcher: typing.Optional[mango.LatestItemObserverSubscriber[mango.PerpMarket]
] = perp_market_watcher ] = perp_market_watcher
self.spot_market_watcher: mango.LatestItemObserverSubscriber[mango.SpotMarket] = spot_market_watcher
self.spot_open_orders_watcher: mango.LatestItemObserverSubscriber[mango.OpenOrders] = spot_open_orders_watcher self.spot_open_orders_watcher: mango.LatestItemObserverSubscriber[mango.OpenOrders] = spot_open_orders_watcher
@property @property
@ -59,10 +57,6 @@ class ModelState:
return None return None
return self.perp_market_watcher.latest return self.perp_market_watcher.latest
@property
def spot_market(self) -> mango.SpotMarket:
return self.spot_market_watcher.latest
@property @property
def spot_open_orders(self) -> mango.OpenOrders: def spot_open_orders(self) -> mango.OpenOrders:
return self.spot_open_orders_watcher.latest return self.spot_open_orders_watcher.latest

View File

@ -0,0 +1,60 @@
# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
# [Docs](https://docs.mango.markets/)
# [Discord](https://discord.gg/67jySBhxrg)
# [Twitter](https://twitter.com/mangomarkets)
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import abc
import logging
import mango
import typing
from .modelstate import ModelState
from .reconciledorders import ReconciledOrders
# # 🥭 OrderReconciler class
#
# Base class for order reconciler that combines existing and desired orders into buckets inside a `ReconciledOrders`.
#
class OrderReconciler(metaclass=abc.ABCMeta):
def __init__(self):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@abc.abstractmethod
def reconcile(self, model_state: ModelState, existing_orders: typing.Sequence[mango.Order], desired_orders: typing.Sequence[mango.Order]) -> ReconciledOrders:
raise NotImplementedError("OrderReconciler.reconcile() is not implemented on the base type.")
def __str__(self) -> str:
return """« 𝙾𝚛𝚍𝚎𝚛𝚁𝚎𝚌𝚘𝚗𝚌𝚒𝚕𝚎𝚛 »"""
def __repr__(self) -> str:
return f"{self}"
# # 🥭 NullOrderReconciler class
#
# Null implementation of OrderReconciler. Just maintains all existing orders.
#
class NullOrderReconciler(OrderReconciler):
def __init__(self):
super().__init__()
def reconcile(self, _: ModelState, existing_orders: typing.Sequence[mango.Order], desired_orders: typing.Sequence[mango.Order]) -> ReconciledOrders:
outcomes: ReconciledOrders = ReconciledOrders()
outcomes.to_keep = list(existing_orders)
outcomes.to_ignore = list(desired_orders)
return outcomes
def __str__(self) -> str:
return """« 𝙽𝚞𝚕𝚕𝙾𝚛𝚍𝚎𝚛𝚁𝚎𝚌𝚘𝚗𝚌𝚒𝚕𝚎𝚛 »"""

View File

@ -0,0 +1,66 @@
# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
# [Docs](https://docs.mango.markets/)
# [Discord](https://discord.gg/67jySBhxrg)
# [Twitter](https://twitter.com/mangomarkets)
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import logging
import mango
import typing
from collections import deque
from .modelstate import ModelState
# # 🥭 OrderTracker class
#
# Maintains a history of orders that were placed (or at least an attempt was made).
#
class OrderTracker:
def __init__(self, max_history: int = 20):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.tracked: typing.Deque[mango.Order] = deque(maxlen=max_history)
def track(self, order: mango.Order):
self.tracked += [order]
def existing_orders(self, model_state: ModelState) -> typing.Sequence[mango.Order]:
live_orders: typing.List[mango.Order] = []
for order_id, client_id in model_state.placed_order_ids:
client_id_int = int(client_id)
details = self._find_tracked(client_id_int)
if details is None:
raise Exception(f"Could not find existing order with client ID {client_id_int}")
order_id_int = int(order_id)
if details.id != order_id_int:
self.tracked.remove(details)
details = details.with_id(order_id_int)
self.tracked += [details]
live_orders += [details]
return live_orders
def _find_tracked(self, client_id_to_find: int) -> typing.Optional[mango.Order]:
for tracked in self.tracked:
if tracked.client_id == client_id_to_find:
return tracked
return None
def __str__(self) -> str:
return """« 𝙾𝚛𝚍𝚎𝚛𝚁𝚎𝚌𝚘𝚗𝚌𝚒𝚕𝚎𝚛 »"""
def __repr__(self) -> str:
return f"{self}"

View File

@ -13,26 +13,29 @@
# [Github](https://github.com/blockworks-foundation) # [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation) # [Email](mailto:hello@blockworks.foundation)
import mango import mango
import typing
from decimal import Decimal
# # 🥭 DesiredOrder class # # 🥭 ReconciledOrders class
# #
# Encapsulates a single order we want to be present on the orderbook. # Desired orders and existing orders are reconciled into:
# * existing orders to keep unchanged
# * existing orders to be cancelled
# * new orders to be placed
# * desired orders to ignore
# #
# This class encapsulates the outcome of such a reconciliation.
class DesiredOrder: #
def __init__(self, side: mango.Side, order_type: mango.OrderType, price: Decimal, quantity: Decimal): class ReconciledOrders:
self.side: mango.Side = side def __init__(self):
self.order_type: mango.OrderType = order_type self.to_keep: typing.List[mango.Order] = []
self.price: Decimal = price self.to_place: typing.List[mango.Order] = []
self.quantity: Decimal = quantity self.to_cancel: typing.List[mango.Order] = []
self.to_ignore: typing.List[mango.Order] = []
def __str__(self) -> str: def __str__(self) -> str:
return f"""« 𝙳𝚎𝚜𝚒𝚛𝚎𝚍𝙾𝚛𝚍𝚎𝚛: {self.order_type} - {self.side} {self.quantity} at {self.price} »""" return f"« 𝚁𝚎𝚌𝚘𝚗𝚌𝚒𝚕𝚎𝚍𝙾𝚛𝚍𝚎𝚛𝚜 [keep: {len(self.to_keep)}, place: {len(self.to_place)}, cancel: {len(self.to_cancel)}, ignore: {len(self.to_ignore)}] »"
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self}" return f"{self}"

View File

@ -86,22 +86,22 @@ class SimpleMarketMaker:
# Calculate what we want the orders to be. # Calculate what we want the orders to be.
bid, ask = self.calculate_order_prices(price) bid, ask = self.calculate_order_prices(price)
buy_size, sell_size = self.calculate_order_sizes(price, inventory) 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()
buy_orders = [order for order in current_orders if order.side == mango.Side.BUY] buy_orders = [order for order in current_orders if order.side == mango.Side.BUY]
if self.orders_require_action(buy_orders, bid, buy_size): if self.orders_require_action(buy_orders, bid, buy_quantity):
self.logger.info("Cancelling BUY orders.") self.logger.info("Cancelling BUY orders.")
for order in buy_orders: for order in buy_orders:
self.market_operations.cancel_order(order) self.market_operations.cancel_order(order)
self.market_operations.place_order(mango.Side.BUY, mango.OrderType.POST_ONLY, bid, buy_size) self.market_operations.place_order(mango.Side.BUY, mango.OrderType.POST_ONLY, bid, buy_quantity)
sell_orders = [order for order in current_orders if order.side == mango.Side.SELL] sell_orders = [order for order in current_orders if order.side == mango.Side.SELL]
if self.orders_require_action(sell_orders, ask, sell_size): if self.orders_require_action(sell_orders, ask, sell_quantity):
self.logger.info("Cancelling SELL orders.") self.logger.info("Cancelling SELL orders.")
for order in sell_orders: for order in sell_orders:
self.market_operations.cancel_order(order) self.market_operations.cancel_order(order)
self.market_operations.place_order(mango.Side.SELL, mango.OrderType.POST_ONLY, ask, sell_size) self.market_operations.place_order(mango.Side.SELL, mango.OrderType.POST_ONLY, ask, sell_quantity)
self.update_health_on_successful_iteration() self.update_health_on_successful_iteration()
except Exception as exception: except Exception as exception:
@ -155,7 +155,7 @@ class SimpleMarketMaker:
return (bid, ask) return (bid, ask)
def calculate_order_sizes(self, price: mango.Price, inventory: typing.Sequence[typing.Optional[mango.TokenValue]]): def calculate_order_quantities(self, price: mango.Price, inventory: typing.Sequence[typing.Optional[mango.TokenValue]]):
base_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.base) base_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.base)
if base_tokens is None: if base_tokens is None:
raise Exception(f"Could not find market-maker base token {price.market.base.symbol} in inventory.") raise Exception(f"Could not find market-maker base token {price.market.base.symbol} in inventory.")
@ -164,15 +164,15 @@ class SimpleMarketMaker:
if quote_tokens is None: if quote_tokens is None:
raise Exception(f"Could not find market-maker quote token {price.market.quote.symbol} in inventory.") raise Exception(f"Could not find market-maker quote token {price.market.quote.symbol} in inventory.")
buy_size = base_tokens.value * self.position_size_ratio buy_quantity = base_tokens.value * self.position_size_ratio
sell_size = (quote_tokens.value / price.mid_price) * self.position_size_ratio sell_quantity = (quote_tokens.value / price.mid_price) * self.position_size_ratio
return (buy_size, sell_size) return (buy_quantity, sell_quantity)
def orders_require_action(self, orders: typing.Sequence[mango.Order], price: Decimal, size: Decimal) -> bool: def orders_require_action(self, orders: typing.Sequence[mango.Order], price: Decimal, quantity: Decimal) -> bool:
def within_tolerance(target_value, order_value, tolerance): def within_tolerance(target_value, order_value, tolerance):
tolerated = order_value * tolerance tolerated = order_value * tolerance
return (order_value < (target_value + tolerated)) and (order_value > (target_value - tolerated)) return (order_value < (target_value + tolerated)) and (order_value > (target_value - tolerated))
return len(orders) == 0 or not all([(within_tolerance(price, order.price, self.existing_order_tolerance)) and within_tolerance(size, order.size, self.existing_order_tolerance) for order in orders]) 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) for order in orders])
def update_health_on_successful_iteration(self) -> None: def update_health_on_successful_iteration(self) -> None:
try: try:

View File

@ -0,0 +1,100 @@
# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
# [Docs](https://docs.mango.markets/)
# [Discord](https://discord.gg/67jySBhxrg)
# [Twitter](https://twitter.com/mangomarkets)
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import mango
import typing
from decimal import Decimal
from .modelstate import ModelState
from .orderreconciler import OrderReconciler
from .reconciledorders import ReconciledOrders
# # 🥭 ToleranceOrderReconciler class
#
# Has a level of 'tolerance' around whether a desired order matches an existing order.
#
# There are two tolerance levels:
# * A tolerance for price matching
# * A tolderance for quantity matching
#
# Tolerances are expressed as a ratio. To match the existing value must be within +/- the tolderance
# of the desired value.
#
# Note:
# * A BUY only matches with a BUY, a SELL only matches with a SELL.
# * ID and Client ID are ignored when matching.
# * ModelState is ignored when matching.
#
class ToleranceOrderReconciler(OrderReconciler):
def __init__(self, price_tolerance: Decimal, quantity_tolerance: Decimal):
super().__init__()
self.price_tolerance: Decimal = price_tolerance
self.quantity_tolerance: Decimal = quantity_tolerance
def reconcile(self, _: ModelState, existing_orders: typing.Sequence[mango.Order], desired_orders: typing.Sequence[mango.Order]) -> ReconciledOrders:
remaining_existing_orders: typing.List[mango.Order] = list(existing_orders)
outcomes: ReconciledOrders = ReconciledOrders()
for desired in desired_orders:
acceptable = self.find_acceptable_order(desired, remaining_existing_orders)
if acceptable is None:
outcomes.to_place += [desired]
else:
outcomes.to_keep += [acceptable]
outcomes.to_ignore += [desired]
remaining_existing_orders.remove(acceptable)
# By this point we have removed all acceptable existing orders, so those that remain
# should be cancelled.
outcomes.to_cancel = remaining_existing_orders
in_count = len(existing_orders) + len(desired_orders)
out_count = len(outcomes.to_place) + len(outcomes.to_cancel) + len(outcomes.to_keep) + len(outcomes.to_ignore)
if in_count != out_count:
raise Exception(
f"Failure processing all desired orders. Count of orders in: {in_count}. Count of orders out: {out_count}.")
return outcomes
def find_acceptable_order(self, desired: mango.Order, existing_orders: typing.Sequence[mango.Order]) -> typing.Optional[mango.Order]:
for existing in existing_orders:
if self.is_within_tolderance(existing, desired):
return existing
return None
def is_within_tolderance(self, existing: mango.Order, desired: mango.Order) -> bool:
if existing.side != desired.side:
return False
price_tolerance: Decimal = existing.price * self.price_tolerance
if desired.price > (existing.price + price_tolerance):
return False
if desired.price < (existing.price - price_tolerance):
return False
quantity_tolerance: Decimal = existing.quantity * self.quantity_tolerance
if desired.quantity > (existing.quantity + quantity_tolerance):
return False
if desired.quantity < (existing.quantity - quantity_tolerance):
return False
return True
def __str__(self) -> str:
return f"« 𝚃𝚘𝚕𝚎𝚛𝚊𝚗𝚌𝚎𝙾𝚛𝚍𝚎𝚛𝚁𝚎𝚌𝚘𝚗𝚌𝚒𝚕𝚎𝚛 [price tolerance: {self.price_tolerance}, quantity tolerance: {self.quantity_tolerance}] »"

View File

@ -29,7 +29,7 @@ from .orders import Order, OrderType, Side
# This file deals with placing orders. We want the interface to be simple and basic: # This file deals with placing orders. We want the interface to be simple and basic:
# ``` # ```
# order_placer.cancel_order(context, market) # order_placer.cancel_order(context, market)
# order_placer.place_order(context, market, side, order_type, price, size) # order_placer.place_order(context, market, side, order_type, price, quantity)
# ``` # ```
# This requires the `MarketOperations` already know a bit about the market it is placing the # This requires the `MarketOperations` already know a bit about the market it is placing the
# order on, and the code in the `MarketOperations` be specialised for that market platform. # order on, and the code in the `MarketOperations` be specialised for that market platform.
@ -46,7 +46,7 @@ from .orders import Order, OrderType, Side
# Whichever choice is made, the calling code shouldn't have to care. It should be able to # Whichever choice is made, the calling code shouldn't have to care. It should be able to
# use its `MarketOperations` class as simply as: # use its `MarketOperations` class as simply as:
# ``` # ```
# order_placer.place_order(context, side, order_type, price, size) # order_placer.place_order(context, side, order_type, price, quantity)
# ``` # ```
# #
@ -60,7 +60,7 @@ class MarketOperations(metaclass=abc.ABCMeta):
raise NotImplementedError("MarketOperations.cancel_order() is not implemented on the base type.") raise NotImplementedError("MarketOperations.cancel_order() is not implemented on the base type.")
@abc.abstractmethod @abc.abstractmethod
def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order: def place_order(self, side: Side, order_type: OrderType, price: Decimal, quantity: Decimal) -> Order:
raise NotImplementedError("MarketOperations.place_order() is not implemented on the base type.") raise NotImplementedError("MarketOperations.place_order() is not implemented on the base type.")
@abc.abstractmethod @abc.abstractmethod
@ -88,13 +88,13 @@ class NullMarketOperations(MarketOperations):
def cancel_order(self, order: Order) -> typing.Sequence[str]: def cancel_order(self, order: Order) -> typing.Sequence[str]:
self.logger.info( self.logger.info(
f"Cancelling order {order.id} for size {order.size} at price {order.price} on market {self.market_name} with client ID {order.client_id}.") f"Cancelling order {order.id} for quantity {order.quantity} at price {order.price} on market {self.market_name} with client ID {order.client_id}.")
return [""] return [""]
def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order: def place_order(self, side: Side, order_type: OrderType, price: Decimal, quantity: Decimal) -> Order:
self.logger.info( self.logger.info(
f"Placing {order_type} {side} order for size {size} at price {price} on market {self.market_name}.") f"Placing {order_type} {side} order for quantity {quantity} at price {price} on market {self.market_name}.")
return Order(id=0, side=side, price=price, size=size, client_id=0, owner=SYSTEM_PROGRAM_ADDRESS) return Order(id=0, side=side, price=price, quantity=quantity, client_id=0, owner=SYSTEM_PROGRAM_ADDRESS, order_type=order_type)
def load_orders(self) -> typing.Sequence[Order]: def load_orders(self) -> typing.Sequence[Order]:
return [] return []

View File

@ -20,6 +20,7 @@ import rx.subject
import typing import typing
from datetime import datetime from datetime import datetime
from pathlib import Path
from rx.core.abc.disposable import Disposable from rx.core.abc.disposable import Disposable
from rxpy_backpressure import BackPressure from rxpy_backpressure import BackPressure
@ -321,3 +322,34 @@ class DisposePropagator(Disposable):
def dispose(self): def dispose(self):
for disposable in self.disposables: for disposable in self.disposables:
disposable.dispose() disposable.dispose()
# # 🥭 FileToucherObserver class
#
# An `Observer` that touches a file every time an item is observed, and deletes that file when the
# `Observable` completes.
#
# The use case for this is for things like health checks. If a file like /var/tmp/helathz is touched
# every time an item is processed, systems running contianer images can watch for these files and
# trigger alerts or restarts if the file hasn't been touched within a certain time limit.
#
class FileToucherObserver(rx.core.typing.Observer):
def __init__(self, filename: str):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.filename = filename
def on_next(self, _: typing.Any) -> None:
try:
Path(self.filename).touch(mode=0o666, exist_ok=True)
except Exception as exception:
self.logger.warning(f"Touching file '{self.filename}' raised exception: {exception}")
def on_error(self, exception: Exception) -> None:
self.logger.warning(f"FileTouchObserver ignoring error: {exception}")
def on_completed(self) -> None:
try:
self.logger.info(f"Cleaning up touch file '{self.filename}'.")
Path(self.filename).unlink(missing_ok=True)
except Exception as exception:
self.logger.warning(f"Deleting touch file '{self.filename}' raised exception: {exception}")

View File

@ -23,6 +23,7 @@ from decimal import Decimal
from pyserum.market.types import Order as SerumOrder from pyserum.market.types import Order as SerumOrder
from solana.publickey import PublicKey from solana.publickey import PublicKey
from .constants import SYSTEM_PROGRAM_ADDRESS
# # 🥭 Orders # # 🥭 Orders
# #
@ -57,6 +58,7 @@ class Side(enum.Enum):
class OrderType(enum.Enum): class OrderType(enum.Enum):
# We use strings here so that argparse can work with these as parameters. # We use strings here so that argparse can work with these as parameters.
UNKNOWN = "UNKNOWN"
LIMIT = "LIMIT" LIMIT = "LIMIT"
IOC = "IOC" IOC = "IOC"
POST_ONLY = "POST_ONLY" POST_ONLY = "POST_ONLY"
@ -80,13 +82,35 @@ class Order(typing.NamedTuple):
owner: PublicKey owner: PublicKey
side: Side side: Side
price: Decimal price: Decimal
size: Decimal quantity: Decimal
order_type: OrderType
# Returns an identical order with the ID changed.
def with_id(self, id: int) -> "Order":
return Order(id=id, side=self.side, price=self.price, quantity=self.quantity,
client_id=self.client_id, owner=self.owner, order_type=self.order_type)
# Returns an identical order with the Client ID changed.
def with_client_id(self, client_id: int) -> "Order":
return Order(id=self.id, side=self.side, price=self.price, quantity=self.quantity,
client_id=client_id, owner=self.owner, order_type=self.order_type)
@staticmethod @staticmethod
def from_serum_order(serum_order: SerumOrder) -> "Order": def from_serum_order(serum_order: SerumOrder) -> "Order":
price = Decimal(serum_order.info.price) price = Decimal(serum_order.info.price)
size = Decimal(serum_order.info.size) quantity = Decimal(serum_order.info.size)
side = Side.BUY if serum_order.side == pyserum.enums.Side.BUY else Side.SELL side = Side.BUY if serum_order.side == pyserum.enums.Side.BUY else Side.SELL
order = Order(id=serum_order.order_id, side=side, price=price, size=size, order = Order(id=serum_order.order_id, side=side, price=price, quantity=quantity,
client_id=serum_order.client_id, owner=serum_order.open_order_address) client_id=serum_order.client_id, owner=serum_order.open_order_address,
order_type=OrderType.UNKNOWN)
return order return order
@staticmethod
def from_basic_info(side: Side, price: Decimal, quantity: Decimal, order_type: OrderType = OrderType.UNKNOWN) -> "Order":
order = Order(id=0, side=side, price=price, quantity=quantity, client_id=0,
owner=SYSTEM_PROGRAM_ADDRESS, order_type=order_type)
return order
@staticmethod
def from_ids(id: int, client_id: int) -> "Order":
return Order(id=id, client_id=client_id, owner=SYSTEM_PROGRAM_ADDRESS, side=Side.BUY, price=Decimal(0), quantity=Decimal(0), order_type=OrderType.UNKNOWN)

View File

@ -22,7 +22,7 @@ from .context import Context
from .group import Group from .group import Group
from .marketinstructionbuilder import MarketInstructionBuilder from .marketinstructionbuilder import MarketInstructionBuilder
from .instructions import build_cancel_perp_order_instructions, build_mango_consume_events_instructions, build_place_perp_order_instructions from .instructions import build_cancel_perp_order_instructions, build_mango_consume_events_instructions, build_place_perp_order_instructions
from .orders import Order, OrderType, Side from .orders import Order
from .perpmarket import PerpMarket from .perpmarket import PerpMarket
from .wallet import Wallet from .wallet import Wallet
@ -53,9 +53,9 @@ class PerpMarketInstructionBuilder(MarketInstructionBuilder):
return build_cancel_perp_order_instructions( return build_cancel_perp_order_instructions(
self.context, self.wallet, self.account, self.perp_market, order) self.context, self.wallet, self.account, self.perp_market, order)
def build_place_order_instructions(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal, client_id: int) -> CombinableInstructions: def build_place_order_instructions(self, order: Order) -> CombinableInstructions:
return build_place_perp_order_instructions( return build_place_perp_order_instructions(
self.context, self.wallet, self.perp_market.group, self.account, self.perp_market, price, size, client_id, side, order_type) self.context, self.wallet, self.perp_market.group, self.account, self.perp_market, order.price, order.quantity, order.client_id, order.side, order.order_type)
def build_settle_instructions(self) -> CombinableInstructions: def build_settle_instructions(self) -> CombinableInstructions:
return CombinableInstructions.empty() return CombinableInstructions.empty()

View File

@ -51,21 +51,22 @@ class PerpMarketOperations(MarketOperations):
def cancel_order(self, order: Order) -> typing.Sequence[str]: def cancel_order(self, order: Order) -> typing.Sequence[str]:
self.logger.info( self.logger.info(
f"Cancelling order {order.id} for size {order.size} at price {order.price} on market {self.market_name} with client ID {order.client_id}.") f"Cancelling order {order.id} for quantity {order.quantity} at price {order.price} on market {self.market_name} with client ID {order.client_id}.")
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
cancel = self.market_instruction_builder.build_cancel_order_instructions(order) cancel = self.market_instruction_builder.build_cancel_order_instructions(order)
return (signers + cancel).execute_and_unwrap_transaction_ids(self.context) return (signers + cancel).execute_and_unwrap_transaction_ids(self.context)
def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order: def place_order(self, side: Side, order_type: OrderType, price: Decimal, quantity: Decimal) -> Order:
client_id: int = self.context.random_client_id() client_id: int = self.context.random_client_id()
self.logger.info( self.logger.info(
f"Placing {order_type} {side} order for size {size} at price {price} on market {self.market_name} with ID {client_id}.") f"Placing {order_type} {side} order for quantity {quantity} at price {price} on market {self.market_name} with ID {client_id}.")
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
place = self.market_instruction_builder.build_place_order_instructions( order = Order(id=0, client_id=client_id, owner=self.account.address,
side, order_type, price, size, client_id) side=side, price=price, quantity=quantity, order_type=order_type)
place = self.market_instruction_builder.build_place_order_instructions(order)
(signers + place).execute(self.context) (signers + place).execute(self.context)
return Order(id=0, side=side, price=price, size=size, client_id=client_id, owner=self.account.address) return order
def load_orders(self) -> typing.Sequence[Order]: def load_orders(self) -> typing.Sequence[Order]:
bids_address: PublicKey = self.perp_market.bids bids_address: PublicKey = self.perp_market.bids

View File

@ -92,7 +92,7 @@ class SerumMarketInstructionBuilder(MarketInstructionBuilder):
) )
return CombinableInstructions.from_instruction(raw_instruction) return CombinableInstructions.from_instruction(raw_instruction)
def build_place_order_instructions(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal, client_id: int) -> CombinableInstructions: def build_place_order_instructions(self, order: Order) -> CombinableInstructions:
ensure_open_orders = CombinableInstructions.empty() ensure_open_orders = CombinableInstructions.empty()
if self.open_orders_address is None: if self.open_orders_address is None:
ensure_open_orders = build_create_serum_open_orders_instructions( ensure_open_orders = build_create_serum_open_orders_instructions(
@ -100,12 +100,12 @@ class SerumMarketInstructionBuilder(MarketInstructionBuilder):
self.open_orders_address = ensure_open_orders.signers[0].public_key() self.open_orders_address = ensure_open_orders.signers[0].public_key()
serum_order_type = pyserum.enums.OrderType.POST_ONLY if order_type == OrderType.POST_ONLY else pyserum.enums.OrderType.IOC if order_type == OrderType.IOC else pyserum.enums.OrderType.LIMIT serum_order_type = pyserum.enums.OrderType.POST_ONLY if order.order_type == OrderType.POST_ONLY else pyserum.enums.OrderType.IOC if order.order_type == OrderType.IOC else pyserum.enums.OrderType.LIMIT
serum_side = pyserum.enums.Side.BUY if side == Side.BUY else pyserum.enums.Side.SELL serum_side = pyserum.enums.Side.BUY if order.side == Side.BUY else pyserum.enums.Side.SELL
payer_token_account = self.quote_token_account if side == Side.BUY else self.base_token_account payer_token_account = self.quote_token_account if order.side == Side.BUY else self.base_token_account
raw_instruction = self.raw_market.make_place_order_instruction(payer_token_account.address, self.wallet.account, serum_order_type, serum_side, float( raw_instruction = self.raw_market.make_place_order_instruction(payer_token_account.address, self.wallet.account, serum_order_type, serum_side, float(
price), float(size), client_id, self.open_orders_address, self.fee_discount_token_address) order.price), float(order.quantity), order.client_id, self.open_orders_address, self.fee_discount_token_address)
place = CombinableInstructions.from_instruction(raw_instruction) place = CombinableInstructions.from_instruction(raw_instruction)

View File

@ -49,29 +49,30 @@ class SerumMarketOperations(MarketOperations):
def cancel_order(self, order: Order) -> typing.Sequence[str]: def cancel_order(self, order: Order) -> typing.Sequence[str]:
self.logger.info( self.logger.info(
f"Cancelling order {order.id} for size {order.size} at price {order.price} on market {self.serum_market.symbol} with client ID {order.client_id}.") f"Cancelling order {order.id} for quantity {order.quantity} at price {order.price} on market {self.serum_market.symbol} with client ID {order.client_id}.")
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
cancel = self.market_instruction_builder.build_cancel_order_instructions(order) cancel = self.market_instruction_builder.build_cancel_order_instructions(order)
crank = self.market_instruction_builder.build_crank_instructions() crank = self.market_instruction_builder.build_crank_instructions()
settle = self.market_instruction_builder.build_settle_instructions() settle = self.market_instruction_builder.build_settle_instructions()
return (signers + cancel + crank + settle).execute_and_unwrap_transaction_ids(self.context) return (signers + cancel + crank + settle).execute_and_unwrap_transaction_ids(self.context)
def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order: def place_order(self, side: Side, order_type: OrderType, price: Decimal, quantity: Decimal) -> Order:
client_id: int = self.context.random_client_id() client_id: int = self.context.random_client_id()
self.logger.info( self.logger.info(
f"Placing {order_type} {side} order for size {size} at price {price} on market {self.serum_market.symbol} with client ID {client_id}.") f"Placing {order_type} {side} order for quantity {quantity} at price {price} on market {self.serum_market.symbol} with client ID {client_id}.")
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
place = self.market_instruction_builder.build_place_order_instructions( open_orders_address = self.market_instruction_builder.open_orders_address or SYSTEM_PROGRAM_ADDRESS
side, order_type, price, size, client_id) order = Order(id=0, client_id=client_id, side=side, price=price,
quantity=quantity, owner=open_orders_address, order_type=order_type)
place = self.market_instruction_builder.build_place_order_instructions(order)
crank = self.market_instruction_builder.build_crank_instructions() crank = self.market_instruction_builder.build_crank_instructions()
settle = self.market_instruction_builder.build_settle_instructions() settle = self.market_instruction_builder.build_settle_instructions()
(signers + place + crank + settle).execute(self.context) (signers + place + crank + settle).execute(self.context)
open_orders_address = self.market_instruction_builder.open_orders_address or SYSTEM_PROGRAM_ADDRESS return order
return Order(id=0, side=side, price=price, size=size, client_id=client_id, owner=open_orders_address)
def _load_serum_orders(self) -> typing.Sequence[SerumOrder]: def _load_serum_orders(self) -> typing.Sequence[SerumOrder]:
raw_market = self.market_instruction_builder.raw_market raw_market = self.market_instruction_builder.raw_market

View File

@ -23,9 +23,9 @@ from .account import Account
from .combinableinstructions import CombinableInstructions from .combinableinstructions import CombinableInstructions
from .context import Context from .context import Context
from .group import Group from .group import Group
from .instructions import build_compound_spot_place_order_instructions, build_cancel_spot_order_instructions from .instructions import build_compound_spot_place_order_instructions, build_spot_place_order_instructions, build_cancel_spot_order_instructions
from .marketinstructionbuilder import MarketInstructionBuilder from .marketinstructionbuilder import MarketInstructionBuilder
from .orders import Order, OrderType, Side from .orders import Order, Side
from .spotmarket import SpotMarket from .spotmarket import SpotMarket
from .tokenaccount import TokenAccount from .tokenaccount import TokenAccount
from .wallet import Wallet from .wallet import Wallet
@ -84,11 +84,11 @@ class SpotMarketInstructionBuilder(MarketInstructionBuilder):
return build_cancel_spot_order_instructions( return build_cancel_spot_order_instructions(
self.context, self.wallet, self.group, self.account, self.raw_market, order, open_orders) self.context, self.wallet, self.group, self.account, self.raw_market, order, open_orders)
def build_place_order_instructions(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal, client_id: int) -> CombinableInstructions: def build_place_order_instructions(self, order: Order) -> CombinableInstructions:
payer_token_account = self.quote_token_account if side == Side.BUY else self.base_token_account return build_spot_place_order_instructions(self.context, self.wallet, self.group, self.account,
return build_compound_spot_place_order_instructions( self.raw_market, order.order_type, order.side, order.price,
self.context, self.wallet, self.group, self.account, self.raw_market, payer_token_account.address, order.quantity, order.client_id,
order_type, side, price, size, client_id, self.fee_discount_token_address) self.fee_discount_token_address)
def build_settle_instructions(self) -> CombinableInstructions: def build_settle_instructions(self) -> CombinableInstructions:
return CombinableInstructions.empty() return CombinableInstructions.empty()

View File

@ -53,19 +53,20 @@ class SpotMarketOperations(MarketOperations):
def cancel_order(self, order: Order) -> typing.Sequence[str]: def cancel_order(self, order: Order) -> typing.Sequence[str]:
self.logger.info( self.logger.info(
f"Cancelling order {order.id} for size {order.size} at price {order.price} on market {self.spot_market.symbol} with client ID {order.client_id}.") f"Cancelling order {order.id} for quantity {order.quantity} at price {order.price} on market {self.spot_market.symbol} with client ID {order.client_id}.")
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
cancel = self.market_instruction_builder.build_cancel_order_instructions(order) cancel = self.market_instruction_builder.build_cancel_order_instructions(order)
return (signers + cancel).execute_and_unwrap_transaction_ids(self.context) return (signers + cancel).execute_and_unwrap_transaction_ids(self.context)
def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order: def place_order(self, side: Side, order_type: OrderType, price: Decimal, quantity: Decimal) -> Order:
client_id: int = self.context.random_client_id() client_id: int = self.context.random_client_id()
self.logger.info( self.logger.info(
f"Placing {order_type} {side} order for size {size} at price {price} on market {self.spot_market.symbol} with ID {client_id}.") f"Placing {order_type} {side} order for quantity {quantity} at price {price} on market {self.spot_market.symbol} with ID {client_id}.")
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
place = self.market_instruction_builder.build_place_order_instructions( order = Order(id=0, client_id=client_id, side=side, price=price,
side, order_type, price, size, client_id) quantity=quantity, owner=self.open_orders, order_type=order_type)
place = self.market_instruction_builder.build_place_order_instructions(order)
crank = self.market_instruction_builder.build_crank_instructions() crank = self.market_instruction_builder.build_crank_instructions()
@ -73,7 +74,7 @@ class SpotMarketOperations(MarketOperations):
(signers + place + crank + settle).execute(self.context) (signers + place + crank + settle).execute(self.context)
return Order(id=0, side=side, price=price, size=size, client_id=client_id, owner=self.open_orders) return order
def _load_serum_orders(self) -> typing.Sequence[SerumOrder]: def _load_serum_orders(self) -> typing.Sequence[SerumOrder]:
raw_market = self.market_instruction_builder.raw_market raw_market = self.market_instruction_builder.raw_market

View File

View File

@ -0,0 +1,22 @@
import mango
from decimal import Decimal
from mango.marketmaking.orderreconciler import NullOrderReconciler
def test_nulloperation():
existing = [
mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)),
mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(2), quantity=Decimal(20))
]
desired = [
mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(3), quantity=Decimal(30)),
mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(4), quantity=Decimal(40))
]
actual = NullOrderReconciler()
result = actual.reconcile(None, existing, desired)
assert result.to_keep == existing
assert result.to_ignore == desired

View File

@ -0,0 +1,222 @@
import mango
from decimal import Decimal
from mango.marketmaking.toleranceorderreconciler import ToleranceOrderReconciler
def test_buy_does_not_match_sell():
actual = ToleranceOrderReconciler(Decimal(1), Decimal(1))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(1), quantity=Decimal(10))
assert not actual.is_within_tolderance(existing, desired)
def test_exact_match_with_small_tolerance_matches():
actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal("0.001"))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
assert actual.is_within_tolderance(existing, desired)
def test_exact_match_with_zero_tolerance_matches():
actual = ToleranceOrderReconciler(Decimal(0), Decimal(0))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
assert actual.is_within_tolderance(existing, desired)
def test_quantity_within_positive_tolerance_matches():
actual = ToleranceOrderReconciler(Decimal(0), Decimal("0.001"))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal("10.009"))
assert actual.is_within_tolderance(existing, desired)
def test_quantity_positive_tolerance_boundary_matches():
actual = ToleranceOrderReconciler(Decimal(0), Decimal("0.001"))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal("10.01"))
assert actual.is_within_tolderance(existing, desired)
def test_quantity_outside_positive_tolerance_no_match():
actual = ToleranceOrderReconciler(Decimal(0), Decimal("0.001"))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal("10.011"))
assert not actual.is_within_tolderance(existing, desired)
def test_quantity_within_negative_tolerance_matches():
actual = ToleranceOrderReconciler(Decimal(0), Decimal("0.001"))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal("9.991"))
assert actual.is_within_tolderance(existing, desired)
def test_quantity_negative_tolerance_boundary_matches():
actual = ToleranceOrderReconciler(Decimal(0), Decimal("0.001"))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal("9.99"))
assert actual.is_within_tolderance(existing, desired)
def test_quantity_outside_negative_tolerance_no_match():
actual = ToleranceOrderReconciler(Decimal(0), Decimal("0.001"))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal("9.989"))
assert not actual.is_within_tolderance(existing, desired)
def test_price_within_positive_tolerance_matches():
actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal(0))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("1.0009"), quantity=Decimal(10))
assert actual.is_within_tolderance(existing, desired)
def test_price_positive_tolerance_boundary_matches():
actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal(0))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("1.001"), quantity=Decimal(10))
assert actual.is_within_tolderance(existing, desired)
def test_price_outside_positive_tolerance_no_match():
actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal(0))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("1.0011"), quantity=Decimal(10))
assert not actual.is_within_tolderance(existing, desired)
def test_price_within_negative_tolerance_matches():
actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal(0))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("0.9991"), quantity=Decimal(10))
assert actual.is_within_tolderance(existing, desired)
def test_price_negative_tolerance_boundary_matches():
actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal(0))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("0.999"), quantity=Decimal(10))
assert actual.is_within_tolderance(existing, desired)
def test_price_outside_negative_tolerance_no_match():
actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal(0))
existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10))
desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("0.9989"), quantity=Decimal(10))
assert not actual.is_within_tolderance(existing, desired)
def test_reconcile_no_acceptable_orders():
existing = [
mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(99), quantity=Decimal(10)),
mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(101), quantity=Decimal(10))
]
desired = [
mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(100), quantity=Decimal(10)),
mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(102), quantity=Decimal(10))
]
actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal("0.001"))
result = actual.reconcile(None, existing, desired)
assert result.to_place == desired
assert result.to_cancel == existing
assert result.to_keep == []
assert result.to_ignore == []
def test_reconcile_all_acceptable_orders():
existing = [
mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(99), quantity=Decimal(10)),
mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(101), quantity=Decimal(10))
]
desired = [
mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("99.01"), quantity=Decimal(10)),
mango.Order.from_basic_info(mango.Side.SELL, price=Decimal("101.01"), quantity=Decimal(10))
]
actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal("0.001"))
result = actual.reconcile(None, existing, desired)
assert result.to_place == []
assert result.to_cancel == []
assert result.to_keep == existing
assert result.to_ignore == desired
def test_reconcile_different_list_sizes_orders():
existing = [
mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(98), quantity=Decimal(20)),
mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(99), quantity=Decimal(10)),
mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(101), quantity=Decimal(10)),
mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(102), quantity=Decimal(20))
]
desired = [
mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("99.01"), quantity=Decimal(10)),
mango.Order.from_basic_info(mango.Side.SELL, price=Decimal("101.01"), quantity=Decimal(10))
]
actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal("0.001"))
result = actual.reconcile(None, existing, desired)
assert result.to_place == []
assert len(result.to_cancel) == 2
assert result.to_cancel[0] == existing[0]
assert result.to_cancel[1] == existing[3]
assert len(result.to_keep) == 2
assert result.to_keep[0] == existing[1]
assert result.to_keep[1] == existing[2]
assert result.to_ignore == desired
def test_reconcile_two_acceptable_two_unacceptable_orders():
existing = [
mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(98), quantity=Decimal(20)),
mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(99), quantity=Decimal(10)),
mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(101), quantity=Decimal(10)),
mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(102), quantity=Decimal(20))
]
desired = [
mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("98.1"), quantity=Decimal(20)),
mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("99.01"), quantity=Decimal(10)),
mango.Order.from_basic_info(mango.Side.SELL, price=Decimal("101.01"), quantity=Decimal(10)),
mango.Order.from_basic_info(mango.Side.SELL, price=Decimal("102.11"), quantity=Decimal(20))
]
actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal("0.001"))
result = actual.reconcile(None, existing, desired)
assert len(result.to_place) == 2
assert len(result.to_cancel) == 2
assert len(result.to_keep) == 2
assert len(result.to_ignore) == 2
# Desired 1 outcomes
assert result.to_place[0] == desired[0]
assert result.to_cancel[0] == existing[0]
# Desired 2 outcomes
assert result.to_keep[0] == existing[1]
assert result.to_ignore[0] == desired[1]
# Desired 3 outcomes
assert result.to_keep[1] == existing[2]
assert result.to_ignore[1] == desired[2]
# Desired 4 outcomes
assert result.to_place[1] == desired[3]
assert result.to_cancel[1] == existing[3]