More realistic market-maker is now available.
This commit is contained in:
parent
9960713d7d
commit
7db8326a7c
|
@ -6,8 +6,6 @@ import os
|
|||
import os.path
|
||||
import sys
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
sys.path.insert(0, os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..")))
|
||||
import mango # nopep8
|
||||
|
@ -32,7 +30,6 @@ if market is None:
|
|||
raise Exception(f"Could not find market {market_symbol}")
|
||||
|
||||
market_operations = mango.create_market_operations(context, wallet, False, market)
|
||||
order = mango.Order(id=args.order_id, client_id=0, owner=wallet.address,
|
||||
side=mango.Side.BUY, price=Decimal(0), size=Decimal(0))
|
||||
order = mango.Order.from_ids(id=args.order_id, client_id=0)
|
||||
cancellation = market_operations.cancel_order(order)
|
||||
print(cancellation)
|
||||
|
|
|
@ -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.")
|
|
@ -24,7 +24,7 @@ from .marketlookup import MarketLookup, CompoundMarketLookup
|
|||
from .marketoperations import MarketOperations, NullMarketOperations
|
||||
from .metadata import Metadata
|
||||
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 .orderbookside import OrderBookSide
|
||||
from .orders import Order, OrderType, Side
|
||||
|
|
|
@ -91,7 +91,8 @@ class CombinableInstructions():
|
|||
current_chunk: typing.List[TransactionInstruction] = []
|
||||
for instruction in self.instructions:
|
||||
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
|
||||
else:
|
||||
vetted_chunks += [current_chunk]
|
||||
|
@ -99,9 +100,18 @@ class CombinableInstructions():
|
|||
|
||||
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:
|
||||
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 = []
|
||||
for chunk in all_chunks:
|
||||
transaction = Transaction()
|
||||
|
|
|
@ -50,7 +50,7 @@ from .wallet import Wallet
|
|||
# to send to Solana.
|
||||
#
|
||||
# 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
|
||||
# instruction size limitiations, so all our functions are prepared for this without having to change
|
||||
|
|
|
@ -20,7 +20,7 @@ import logging
|
|||
from decimal import Decimal
|
||||
|
||||
from .combinableinstructions import CombinableInstructions
|
||||
from .orders import Order, OrderType, Side
|
||||
from .orders import Order
|
||||
|
||||
|
||||
# # 🥭 MarketInstructionBuilder class
|
||||
|
@ -50,7 +50,7 @@ class MarketInstructionBuilder(metaclass=abc.ABCMeta):
|
|||
"MarketInstructionBuilder.build_cancel_order_instructions() is not implemented on the base type.")
|
||||
|
||||
@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(
|
||||
"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:
|
||||
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()
|
||||
|
||||
def build_settle_instructions(self) -> CombinableInstructions:
|
||||
|
|
|
@ -19,7 +19,6 @@ import logging
|
|||
import mango
|
||||
import typing
|
||||
|
||||
from .desiredorder import DesiredOrder
|
||||
from .modelstate import ModelState
|
||||
|
||||
|
||||
|
@ -35,7 +34,7 @@ class DesiredOrdersBuilder(metaclass=abc.ABCMeta):
|
|||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
@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.")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
@ -51,7 +50,7 @@ class NullDesiredOrdersBuilder(DesiredOrdersBuilder):
|
|||
def __init__(self):
|
||||
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 []
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
|
@ -20,46 +20,52 @@ import typing
|
|||
|
||||
from decimal import Decimal
|
||||
|
||||
from .desiredorder import DesiredOrder
|
||||
from .desiredordersbuilder import DesiredOrdersBuilder
|
||||
from .modelstate import ModelState
|
||||
|
||||
|
||||
# # 🥭 FixedRatioDesiredOrdersBuilder class
|
||||
# # 🥭 FixedRatiosDesiredOrdersBuilder class
|
||||
#
|
||||
# Builds orders using a fixed spread ratio and a fixed position size ratio.
|
||||
#
|
||||
|
||||
class FixedRatioDesiredOrdersBuilder(DesiredOrdersBuilder):
|
||||
def __init__(self, spread_ratio: Decimal, position_size_ratio: Decimal):
|
||||
class FixedRatiosDesiredOrdersBuilder(DesiredOrdersBuilder):
|
||||
def __init__(self, spread_ratios: typing.Sequence[Decimal], position_size_ratios: typing.Sequence[Decimal]):
|
||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||
self.spread_ratio: Decimal = spread_ratio
|
||||
self.position_size_ratio: Decimal = position_size_ratio
|
||||
if len(spread_ratios) != len(position_size_ratios):
|
||||
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
|
||||
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)
|
||||
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)
|
||||
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
|
||||
position_size = total * self.position_size_ratio
|
||||
|
||||
buy_size: Decimal = position_size / price.mid_price
|
||||
sell_size: Decimal = position_size / price.mid_price
|
||||
orders: typing.List[mango.Order] = []
|
||||
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)
|
||||
ask: Decimal = price.mid_price + (price.mid_price * self.spread_ratio)
|
||||
position_size = total * position_size_ratio
|
||||
buy_quantity: Decimal = position_size / price.mid_price
|
||||
sell_quantity: Decimal = position_size / price.mid_price
|
||||
|
||||
return [
|
||||
DesiredOrder(mango.Side.BUY, mango.OrderType.POST_ONLY, bid, buy_size),
|
||||
DesiredOrder(mango.Side.SELL, mango.OrderType.POST_ONLY, ask, sell_size)
|
||||
bid: Decimal = price.mid_price - (price.mid_price * spread_ratio)
|
||||
ask: Decimal = price.mid_price + (price.mid_price * spread_ratio)
|
||||
|
||||
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:
|
||||
return f"« 𝙵𝚒𝚡𝚎𝚍𝚁𝚊𝚝𝚒𝚘𝙳𝚎𝚜𝚒𝚛𝚎𝚍𝙾𝚛𝚍𝚎𝚛𝚜𝙱𝚞𝚒𝚕𝚍𝚎𝚛 using ratios - spread: {self.spread_ratio}, position size: {self.position_size_ratio} »"
|
|
@ -19,10 +19,10 @@ import mango
|
|||
import traceback
|
||||
import typing
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from .desiredordersbuilder import DesiredOrdersBuilder
|
||||
from .modelstate import ModelState
|
||||
from .orderreconciler import OrderReconciler
|
||||
from .ordertracker import OrderTracker
|
||||
|
||||
|
||||
# # 🥭 MarketMaker class
|
||||
|
@ -33,48 +33,44 @@ from .modelstate import ModelState
|
|||
class MarketMaker:
|
||||
def __init__(self, wallet: mango.Wallet, market: mango.Market,
|
||||
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.wallet: mango.Wallet = wallet
|
||||
self.market: mango.Market = market
|
||||
self.market_instruction_builder: mango.MarketInstructionBuilder = market_instruction_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.sell_client_ids: typing.List[int] = []
|
||||
|
||||
def pulse(self, context: mango.Context, model_state: ModelState):
|
||||
try:
|
||||
desired_orders = self.desired_orders_builder.build(context, model_state)
|
||||
|
||||
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()
|
||||
for order_id, client_id in model_state.placed_order_ids:
|
||||
if client_id != 0:
|
||||
self.logger.info(f"Cancelling order with client ID: {client_id}")
|
||||
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)
|
||||
for to_cancel in reconciled.to_cancel:
|
||||
cancel = self.market_instruction_builder.build_cancel_order_instructions(to_cancel)
|
||||
cancellations += cancel
|
||||
|
||||
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()
|
||||
if desired_order.side == mango.Side.BUY:
|
||||
self.buy_client_ids += [desired_client_id]
|
||||
else:
|
||||
self.sell_client_ids += [desired_client_id]
|
||||
to_place_with_client_id = to_place.with_client_id(desired_client_id)
|
||||
self.order_tracker.track(to_place_with_client_id)
|
||||
|
||||
self.logger.info(
|
||||
f"Placing {desired_order.side} order for {desired_order.quantity} at price {desired_order.price} with client ID: {desired_client_id}")
|
||||
place_order = self.market_instruction_builder.build_place_order_instructions(
|
||||
desired_order.side, desired_order.order_type, desired_order.price, desired_order.quantity, 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(to_place_with_client_id)
|
||||
place_orders += place_order
|
||||
|
||||
settle = self.market_instruction_builder.build_settle_instructions()
|
||||
|
||||
crank = self.market_instruction_builder.build_crank_instructions()
|
||||
(payer + cancellations + place_orders + settle + crank).execute(context)
|
||||
except Exception as exception:
|
||||
|
|
|
@ -32,7 +32,6 @@ class ModelState:
|
|||
group_watcher: mango.LatestItemObserverSubscriber[mango.Group],
|
||||
price_watcher: mango.LatestItemObserverSubscriber[mango.Price],
|
||||
perp_market_watcher: typing.Optional[mango.LatestItemObserverSubscriber[mango.PerpMarket]],
|
||||
spot_market_watcher: mango.LatestItemObserverSubscriber[mango.SpotMarket],
|
||||
spot_open_orders_watcher: mango.LatestItemObserverSubscriber[mango.OpenOrders]
|
||||
):
|
||||
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.perp_market_watcher: typing.Optional[mango.LatestItemObserverSubscriber[mango.PerpMarket]
|
||||
] = 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
|
||||
|
||||
@property
|
||||
|
@ -59,10 +57,6 @@ class ModelState:
|
|||
return None
|
||||
return self.perp_market_watcher.latest
|
||||
|
||||
@property
|
||||
def spot_market(self) -> mango.SpotMarket:
|
||||
return self.spot_market_watcher.latest
|
||||
|
||||
@property
|
||||
def spot_open_orders(self) -> mango.OpenOrders:
|
||||
return self.spot_open_orders_watcher.latest
|
||||
|
|
|
@ -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 """« 𝙽𝚞𝚕𝚕𝙾𝚛𝚍𝚎𝚛𝚁𝚎𝚌𝚘𝚗𝚌𝚒𝚕𝚎𝚛 »"""
|
|
@ -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}"
|
|
@ -13,26 +13,29 @@
|
|||
# [Github](https://github.com/blockworks-foundation)
|
||||
# [Email](mailto:hello@blockworks.foundation)
|
||||
|
||||
|
||||
import mango
|
||||
|
||||
from decimal import Decimal
|
||||
import typing
|
||||
|
||||
|
||||
# # 🥭 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
|
||||
#
|
||||
|
||||
class DesiredOrder:
|
||||
def __init__(self, side: mango.Side, order_type: mango.OrderType, price: Decimal, quantity: Decimal):
|
||||
self.side: mango.Side = side
|
||||
self.order_type: mango.OrderType = order_type
|
||||
self.price: Decimal = price
|
||||
self.quantity: Decimal = quantity
|
||||
# This class encapsulates the outcome of such a reconciliation.
|
||||
#
|
||||
class ReconciledOrders:
|
||||
def __init__(self):
|
||||
self.to_keep: typing.List[mango.Order] = []
|
||||
self.to_place: typing.List[mango.Order] = []
|
||||
self.to_cancel: typing.List[mango.Order] = []
|
||||
self.to_ignore: typing.List[mango.Order] = []
|
||||
|
||||
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:
|
||||
return f"{self}"
|
|
@ -86,22 +86,22 @@ class SimpleMarketMaker:
|
|||
|
||||
# Calculate what we want the orders to be.
|
||||
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()
|
||||
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.")
|
||||
for order in buy_orders:
|
||||
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]
|
||||
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.")
|
||||
for order in sell_orders:
|
||||
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()
|
||||
except Exception as exception:
|
||||
|
@ -155,7 +155,7 @@ class SimpleMarketMaker:
|
|||
|
||||
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)
|
||||
if base_tokens is None:
|
||||
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:
|
||||
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
|
||||
sell_size = (quote_tokens.value / price.mid_price) * self.position_size_ratio
|
||||
return (buy_size, sell_size)
|
||||
buy_quantity = base_tokens.value * self.position_size_ratio
|
||||
sell_quantity = (quote_tokens.value / price.mid_price) * self.position_size_ratio
|
||||
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):
|
||||
tolerated = order_value * tolerance
|
||||
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:
|
||||
try:
|
||||
|
|
|
@ -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}] »"
|
|
@ -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:
|
||||
# ```
|
||||
# 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
|
||||
# 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
|
||||
# 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.")
|
||||
|
||||
@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.")
|
||||
|
||||
@abc.abstractmethod
|
||||
|
@ -88,13 +88,13 @@ class NullMarketOperations(MarketOperations):
|
|||
|
||||
def cancel_order(self, order: Order) -> typing.Sequence[str]:
|
||||
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 [""]
|
||||
|
||||
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(
|
||||
f"Placing {order_type} {side} order for size {size} 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)
|
||||
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, quantity=quantity, client_id=0, owner=SYSTEM_PROGRAM_ADDRESS, order_type=order_type)
|
||||
|
||||
def load_orders(self) -> typing.Sequence[Order]:
|
||||
return []
|
||||
|
|
|
@ -20,6 +20,7 @@ import rx.subject
|
|||
import typing
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from rx.core.abc.disposable import Disposable
|
||||
from rxpy_backpressure import BackPressure
|
||||
|
||||
|
@ -321,3 +322,34 @@ class DisposePropagator(Disposable):
|
|||
def dispose(self):
|
||||
for disposable in self.disposables:
|
||||
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}")
|
||||
|
|
|
@ -23,6 +23,7 @@ from decimal import Decimal
|
|||
from pyserum.market.types import Order as SerumOrder
|
||||
from solana.publickey import PublicKey
|
||||
|
||||
from .constants import SYSTEM_PROGRAM_ADDRESS
|
||||
|
||||
# # 🥭 Orders
|
||||
#
|
||||
|
@ -57,6 +58,7 @@ class Side(enum.Enum):
|
|||
|
||||
class OrderType(enum.Enum):
|
||||
# We use strings here so that argparse can work with these as parameters.
|
||||
UNKNOWN = "UNKNOWN"
|
||||
LIMIT = "LIMIT"
|
||||
IOC = "IOC"
|
||||
POST_ONLY = "POST_ONLY"
|
||||
|
@ -80,13 +82,35 @@ class Order(typing.NamedTuple):
|
|||
owner: PublicKey
|
||||
side: Side
|
||||
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
|
||||
def from_serum_order(serum_order: SerumOrder) -> "Order":
|
||||
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
|
||||
order = Order(id=serum_order.order_id, side=side, price=price, size=size,
|
||||
client_id=serum_order.client_id, owner=serum_order.open_order_address)
|
||||
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,
|
||||
order_type=OrderType.UNKNOWN)
|
||||
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)
|
||||
|
|
|
@ -22,7 +22,7 @@ from .context import Context
|
|||
from .group import Group
|
||||
from .marketinstructionbuilder import MarketInstructionBuilder
|
||||
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 .wallet import Wallet
|
||||
|
||||
|
@ -53,9 +53,9 @@ class PerpMarketInstructionBuilder(MarketInstructionBuilder):
|
|||
return build_cancel_perp_order_instructions(
|
||||
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(
|
||||
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:
|
||||
return CombinableInstructions.empty()
|
||||
|
|
|
@ -51,21 +51,22 @@ class PerpMarketOperations(MarketOperations):
|
|||
|
||||
def cancel_order(self, order: Order) -> typing.Sequence[str]:
|
||||
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)
|
||||
cancel = self.market_instruction_builder.build_cancel_order_instructions(order)
|
||||
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()
|
||||
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)
|
||||
place = self.market_instruction_builder.build_place_order_instructions(
|
||||
side, order_type, price, size, client_id)
|
||||
order = Order(id=0, client_id=client_id, owner=self.account.address,
|
||||
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)
|
||||
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]:
|
||||
bids_address: PublicKey = self.perp_market.bids
|
||||
|
|
|
@ -92,7 +92,7 @@ class SerumMarketInstructionBuilder(MarketInstructionBuilder):
|
|||
)
|
||||
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()
|
||||
if self.open_orders_address is None:
|
||||
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()
|
||||
|
||||
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_side = pyserum.enums.Side.BUY if side == Side.BUY else pyserum.enums.Side.SELL
|
||||
payer_token_account = self.quote_token_account if side == Side.BUY else self.base_token_account
|
||||
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 order.side == Side.BUY else pyserum.enums.Side.SELL
|
||||
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(
|
||||
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)
|
||||
|
||||
|
|
|
@ -49,29 +49,30 @@ class SerumMarketOperations(MarketOperations):
|
|||
|
||||
def cancel_order(self, order: Order) -> typing.Sequence[str]:
|
||||
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)
|
||||
cancel = self.market_instruction_builder.build_cancel_order_instructions(order)
|
||||
crank = self.market_instruction_builder.build_crank_instructions()
|
||||
settle = self.market_instruction_builder.build_settle_instructions()
|
||||
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()
|
||||
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)
|
||||
place = self.market_instruction_builder.build_place_order_instructions(
|
||||
side, order_type, price, size, client_id)
|
||||
open_orders_address = self.market_instruction_builder.open_orders_address or SYSTEM_PROGRAM_ADDRESS
|
||||
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()
|
||||
|
||||
settle = self.market_instruction_builder.build_settle_instructions()
|
||||
|
||||
(signers + place + crank + settle).execute(self.context)
|
||||
open_orders_address = self.market_instruction_builder.open_orders_address or SYSTEM_PROGRAM_ADDRESS
|
||||
return Order(id=0, side=side, price=price, size=size, client_id=client_id, owner=open_orders_address)
|
||||
return order
|
||||
|
||||
def _load_serum_orders(self) -> typing.Sequence[SerumOrder]:
|
||||
raw_market = self.market_instruction_builder.raw_market
|
||||
|
|
|
@ -23,9 +23,9 @@ from .account import Account
|
|||
from .combinableinstructions import CombinableInstructions
|
||||
from .context import Context
|
||||
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 .orders import Order, OrderType, Side
|
||||
from .orders import Order, Side
|
||||
from .spotmarket import SpotMarket
|
||||
from .tokenaccount import TokenAccount
|
||||
from .wallet import Wallet
|
||||
|
@ -84,11 +84,11 @@ class SpotMarketInstructionBuilder(MarketInstructionBuilder):
|
|||
return build_cancel_spot_order_instructions(
|
||||
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:
|
||||
payer_token_account = self.quote_token_account if side == Side.BUY else self.base_token_account
|
||||
return build_compound_spot_place_order_instructions(
|
||||
self.context, self.wallet, self.group, self.account, self.raw_market, payer_token_account.address,
|
||||
order_type, side, price, size, client_id, self.fee_discount_token_address)
|
||||
def build_place_order_instructions(self, order: Order) -> CombinableInstructions:
|
||||
return build_spot_place_order_instructions(self.context, self.wallet, self.group, self.account,
|
||||
self.raw_market, order.order_type, order.side, order.price,
|
||||
order.quantity, order.client_id,
|
||||
self.fee_discount_token_address)
|
||||
|
||||
def build_settle_instructions(self) -> CombinableInstructions:
|
||||
return CombinableInstructions.empty()
|
||||
|
|
|
@ -53,19 +53,20 @@ class SpotMarketOperations(MarketOperations):
|
|||
|
||||
def cancel_order(self, order: Order) -> typing.Sequence[str]:
|
||||
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)
|
||||
cancel = self.market_instruction_builder.build_cancel_order_instructions(order)
|
||||
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()
|
||||
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)
|
||||
place = self.market_instruction_builder.build_place_order_instructions(
|
||||
side, order_type, price, size, client_id)
|
||||
order = Order(id=0, client_id=client_id, side=side, price=price,
|
||||
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()
|
||||
|
||||
|
@ -73,7 +74,7 @@ class SpotMarketOperations(MarketOperations):
|
|||
|
||||
(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]:
|
||||
raw_market = self.market_instruction_builder.raw_market
|
||||
|
|
|
@ -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
|
|
@ -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]
|
Loading…
Reference in New Issue