FixedPositionSizeElement can now take multiple fixed position sizes and properly apply them.
This commit is contained in:
parent
d846346fdb
commit
c17d04152b
|
@ -153,7 +153,7 @@ The ‘confidence interval’ is Pyth’s 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`
|
||||||
|
|
||||||
|
|
|
@ -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} »"
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue