From c17d04152b1ed3a1d0761d6489f518072fc8e0ed Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Sat, 23 Oct 2021 13:44:30 +0100 Subject: [PATCH] FixedPositionSizeElement can now take multiple fixed position sizes and properly apply them. --- docs/MarketmakingOrderChain.md | 19 ++++- .../orderchain/fixedpositionsizeelement.py | 67 ++++++++++++++--- .../test_fixedpositionsizeelement.py | 73 +++++++++++++++++-- 3 files changed, 143 insertions(+), 16 deletions(-) diff --git a/docs/MarketmakingOrderChain.md b/docs/MarketmakingOrderChain.md index d836d4d..b5f6912 100644 --- a/docs/MarketmakingOrderChain.md +++ b/docs/MarketmakingOrderChain.md @@ -153,7 +153,7 @@ The β€˜confidence interval’ is Pyth’s expectation of how far from the curren > Accepts parameter: `--fixedpositionsize-value` -The `FixedPositionSizeElement` overrides the position size of all orders it sees, setting them to the fixed value (in the base currency) specified in the parameter. +The `FixedPositionSizeElement` overrides the position size of all orders it sees, setting them to the fixed value (in the base currency) specified in the parameter(s). For example, adding: ``` @@ -161,6 +161,23 @@ For example, adding: ``` to a chain on ETH/USDC will force all BUY and SELL orders to have a position size of 3 ETH. +`--fixedpositionsize-value` can be specified multiple times for configurations running multiple layers or levels of `Order`s. + +Specifying multiple fixed position sizes is designed to work in an intuitive way - the first BUY and SELL take the first specified position size, the second BUY and SELL take the second specified position size and so on. + +But in practice this could be quite confusing, especially if the `Order`s are not well sorted. + +So, to provide a consistent and intuitive processing of `Order`s, this element will: +* Separate BUY and SELL `Order`s +* Sort the BUY and SELL `Order`s from closest to top-of-book to farthest from top-of-book +* Process each BUY and SELL pair using the next specified position size (substituting the last specified position size if no other one is available) + +The result is if you specify `--fixedpositionsize-value 2 --fixedpositionsize-value 4` the BUY and SELL `Order`s closest to top-of-book will both be given a fixed position size of 2, and the next BUY and SELL `Order`s will be given a fixed position size of 4. + +(In fact, in that example, the BUY and SELL nearest the mid-price will be given a position size of 2 and all other `Order`s will be given a position size of 4.) + +Note that one consequence of this processing of `Order`s is that the orders returned from this `Element` may be in a different sort-order to the orders that were sent to it. + ### `FixedSpreadElement` diff --git a/mango/marketmaking/orderchain/fixedpositionsizeelement.py b/mango/marketmaking/orderchain/fixedpositionsizeelement.py index 967124a..c65c776 100644 --- a/mango/marketmaking/orderchain/fixedpositionsizeelement.py +++ b/mango/marketmaking/orderchain/fixedpositionsizeelement.py @@ -29,13 +29,13 @@ from ...modelstate import ModelState # value and a fixed position size value. # class FixedPositionSizeElement(Element): - def __init__(self, position_size: Decimal): + def __init__(self, position_sizes: typing.Sequence[Decimal]): super().__init__() - self.position_size: Decimal = position_size + self.position_sizes: typing.Sequence[Decimal] = position_sizes @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--fixedpositionsize-value", type=Decimal, + parser.add_argument("--fixedpositionsize-value", type=Decimal, action="append", help="fixed value to use as the position size (only works well with a single 'level' of orders - one BUY and one SELL)") @staticmethod @@ -43,20 +43,69 @@ class FixedPositionSizeElement(Element): if args.fixedpositionsize_value is None: raise Exception("No position-size value specified. Try the --fixedpositionsize-value parameter?") - position_size: Decimal = args.fixedpositionsize_value - return FixedPositionSizeElement(position_size) + position_sizes: typing.Sequence[Decimal] = args.fixedpositionsize_value + return FixedPositionSizeElement(position_sizes) - def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: + # This is the simple case. If there is only one fixed position size, just apply it to every order. + def _single_fixed_position_size(self, position_size: Decimal, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: new_orders: typing.List[mango.Order] = [] for order in orders: - new_order: mango.Order = order.with_quantity(self.position_size) + new_order: mango.Order = order.with_quantity(position_size) - self.logger.debug(f"""Order change - using fixed position size of {self.position_size}: + self.logger.debug(f"""Order change - using fixed position size of {position_size}: Old: {order} New: {new_order}""") new_orders += [new_order] return new_orders + # This is the complicated case. If multiple levels are specified, apply them in order. + # + # But 'in order' is complicated. The way it will be expected to work is: + # * First BUY and first SELL get the first fixed position size + # * Second BUY and second SELL get the second fixed position size + # * Third BUY and third SELL get the third fixed position size + # * etc. + # But (another but!) 'first' means closest to the top of the book to people, not necessarily + # first in the incoming order list. + # + # We want to meet that expected approach, so we'll: + # * Split the list into BUYs and SELLs + # * Sort the two lists so closest to top-of-book is at index 0 + # * Process both lists together, setting the appropriate fixed position sizes + def _multiple_fixed_position_size(self, position_sizes: typing.Sequence[Decimal], orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: + buys: typing.List[mango.Order] = list([order for order in orders if order.side == mango.Side.BUY]) + buys.sort(key=lambda order: order.price, reverse=True) + sells: typing.List[mango.Order] = list([order for order in orders if order.side == mango.Side.SELL]) + sells.sort(key=lambda order: order.price) + + pair_count: int = max(len(buys), len(sells)) + new_orders: typing.List[mango.Order] = [] + for index in range(pair_count): + # If no position size is explicitly specified for this element, just use the last specified size. + size: Decimal = position_sizes[index] if index < len(position_sizes) else position_sizes[-1] + if index < len(buys): + buy = buys[index] + new_buy: mango.Order = buy.with_quantity(size) + self.logger.debug(f"""Order change - using fixed position size of {size}: + Old: {buy} + New: {new_buy}""") + new_orders += [new_buy] + + if index < len(sells): + sell = sells[index] + new_sell: mango.Order = sell.with_quantity(size) + self.logger.debug(f"""Order change - using fixed position size of {size}: + Old: {sell} + New: {new_sell}""") + new_orders += [new_sell] + + return new_orders + + def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: + if len(self.position_sizes) == 1: + return self._single_fixed_position_size(self.position_sizes[0], orders) + return self._multiple_fixed_position_size(self.position_sizes, orders) + def __str__(self) -> str: - return f"Β« π™΅πš’πš‘πšŽπšπ™Ώπš˜πšœπš’πšπš’πš˜πš—πš‚πš’πš£πšŽπ™΄πš•πšŽπš–πšŽπš—πš using position size: {self.position_size} Β»" + return f"Β« π™΅πš’πš‘πšŽπšπ™Ώπš˜πšœπš’πšπš’πš˜πš—πš‚πš’πš£πšŽπ™΄πš•πšŽπš–πšŽπš—πš using position sizes: {self.position_sizes} Β»" diff --git a/tests/marketmaking/orderchain/test_fixedpositionsizeelement.py b/tests/marketmaking/orderchain/test_fixedpositionsizeelement.py index 451b69f..80d2aab 100644 --- a/tests/marketmaking/orderchain/test_fixedpositionsizeelement.py +++ b/tests/marketmaking/orderchain/test_fixedpositionsizeelement.py @@ -12,26 +12,87 @@ model_state = fake_model_state(price=fake_price(price=Decimal(80))) def test_from_args(): - args: argparse.Namespace = argparse.Namespace(fixedpositionsize_value=Decimal(17)) + args: argparse.Namespace = argparse.Namespace(fixedpositionsize_value=[Decimal(17)]) actual: FixedPositionSizeElement = FixedPositionSizeElement.from_command_line_parameters(args) - assert actual.position_size == 17 + assert actual.position_sizes == [17] -def test_bid_quantity_updated(): +def test_single_bid_quantity_updated(): context = fake_context() order: mango.Order = fake_order(quantity=Decimal(10), side=mango.Side.BUY) - actual: FixedPositionSizeElement = FixedPositionSizeElement(Decimal(20)) + actual: FixedPositionSizeElement = FixedPositionSizeElement([Decimal(20)]) result = actual.process(context, model_state, [order]) assert result[0].quantity == 20 -def test_ask_quantity_updated(): +def test_single_ask_quantity_updated(): context = fake_context() order: mango.Order = fake_order(quantity=Decimal(11), side=mango.Side.SELL) - actual: FixedPositionSizeElement = FixedPositionSizeElement(Decimal(21)) + actual: FixedPositionSizeElement = FixedPositionSizeElement([Decimal(21)]) result = actual.process(context, model_state, [order]) assert result[0].quantity == 21 + + +def test_single_quantity_multiple_orders_updated(): + context = fake_context() + order1: mango.Order = fake_order(quantity=Decimal(9), side=mango.Side.BUY) + order2: mango.Order = fake_order(quantity=Decimal(10), side=mango.Side.BUY) + order3: mango.Order = fake_order(quantity=Decimal(11), side=mango.Side.SELL) + order4: mango.Order = fake_order(quantity=Decimal(12), side=mango.Side.SELL) + + actual: FixedPositionSizeElement = FixedPositionSizeElement([Decimal(20)]) + result = actual.process(context, model_state, [order1, order2, order3, order4]) + + assert result[0].quantity == 20 + assert result[1].quantity == 20 + assert result[2].quantity == 20 + assert result[3].quantity == 20 + + +def test_three_quantities_six_paired_orders_different_order_updated(): + context = fake_context() + order1: mango.Order = fake_order(quantity=Decimal(8), side=mango.Side.BUY) + order2: mango.Order = fake_order(quantity=Decimal(9), side=mango.Side.BUY) + order3: mango.Order = fake_order(quantity=Decimal(10), side=mango.Side.BUY) + order4: mango.Order = fake_order(quantity=Decimal(11), side=mango.Side.SELL) + order5: mango.Order = fake_order(quantity=Decimal(12), side=mango.Side.SELL) + order6: mango.Order = fake_order(quantity=Decimal(13), side=mango.Side.SELL) + + actual: FixedPositionSizeElement = FixedPositionSizeElement([Decimal(22), Decimal(33), Decimal(44)]) + + # This line is different from previous test - orders are in different order but should be + # returned in the proper order + result = actual.process(context, model_state, [order4, order3, order1, order2, order6, order5]) + + assert result[0].quantity == 22 + assert result[1].quantity == 22 + assert result[2].quantity == 33 + assert result[3].quantity == 33 + assert result[4].quantity == 44 + assert result[5].quantity == 44 + + +def test_two_quantities_six_paired_orders_different_order_updated(): + context = fake_context() + order1: mango.Order = fake_order(quantity=Decimal(8), side=mango.Side.BUY) + order2: mango.Order = fake_order(quantity=Decimal(9), side=mango.Side.BUY) + order3: mango.Order = fake_order(quantity=Decimal(10), side=mango.Side.BUY) + order4: mango.Order = fake_order(quantity=Decimal(11), side=mango.Side.SELL) + order5: mango.Order = fake_order(quantity=Decimal(12), side=mango.Side.SELL) + order6: mango.Order = fake_order(quantity=Decimal(13), side=mango.Side.SELL) + + actual: FixedPositionSizeElement = FixedPositionSizeElement([Decimal(22), Decimal(33)]) + result = actual.process(context, model_state, [order4, order3, order1, order2, order6, order5]) + + assert result[0].quantity == 22 + assert result[1].quantity == 22 + assert result[2].quantity == 33 + assert result[3].quantity == 33 + + # Should just use the last specified size if no other available + assert result[4].quantity == 33 + assert result[5].quantity == 33