FixedPositionSizeElement can now take multiple fixed position sizes and properly apply them.

This commit is contained in:
Geoff Taylor 2021-10-23 13:44:30 +01:00
parent d846346fdb
commit c17d04152b
3 changed files with 143 additions and 16 deletions

View File

@ -153,7 +153,7 @@ The confidence interval is Pyths expectation of how far from the curren
> Accepts parameter: `--fixedpositionsize-value` > 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: 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. 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` ### `FixedSpreadElement`

View File

@ -29,13 +29,13 @@ from ...modelstate import ModelState
# value and a fixed position size value. # value and a fixed position size value.
# #
class FixedPositionSizeElement(Element): class FixedPositionSizeElement(Element):
def __init__(self, position_size: Decimal): def __init__(self, position_sizes: typing.Sequence[Decimal]):
super().__init__() super().__init__()
self.position_size: Decimal = position_size self.position_sizes: typing.Sequence[Decimal] = position_sizes
@staticmethod @staticmethod
def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: 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)") help="fixed value to use as the position size (only works well with a single 'level' of orders - one BUY and one SELL)")
@staticmethod @staticmethod
@ -43,20 +43,69 @@ class FixedPositionSizeElement(Element):
if args.fixedpositionsize_value is None: if args.fixedpositionsize_value is None:
raise Exception("No position-size value specified. Try the --fixedpositionsize-value parameter?") raise Exception("No position-size value specified. Try the --fixedpositionsize-value parameter?")
position_size: Decimal = args.fixedpositionsize_value position_sizes: typing.Sequence[Decimal] = args.fixedpositionsize_value
return FixedPositionSizeElement(position_size) 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] = [] new_orders: typing.List[mango.Order] = []
for order in orders: 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} Old: {order}
New: {new_order}""") New: {new_order}""")
new_orders += [new_order] new_orders += [new_order]
return new_orders 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: def __str__(self) -> str:
return f"« 𝙵𝚒𝚡𝚎𝚍𝙿𝚘𝚜𝚒𝚝𝚒𝚘𝚗𝚂𝚒𝚣𝚎𝙴𝚕𝚎𝚖𝚎𝚗𝚝 using position size: {self.position_size} »" return f"« 𝙵𝚒𝚡𝚎𝚍𝙿𝚘𝚜𝚒𝚝𝚒𝚘𝚗𝚂𝚒𝚣𝚎𝙴𝚕𝚎𝚖𝚎𝚗𝚝 using position sizes: {self.position_sizes} »"

View File

@ -12,26 +12,87 @@ model_state = fake_model_state(price=fake_price(price=Decimal(80)))
def test_from_args(): 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) 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() context = fake_context()
order: mango.Order = fake_order(quantity=Decimal(10), side=mango.Side.BUY) 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]) result = actual.process(context, model_state, [order])
assert result[0].quantity == 20 assert result[0].quantity == 20
def test_ask_quantity_updated(): def test_single_ask_quantity_updated():
context = fake_context() context = fake_context()
order: mango.Order = fake_order(quantity=Decimal(11), side=mango.Side.SELL) 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]) result = actual.process(context, model_state, [order])
assert result[0].quantity == 21 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