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`
|
||||
|
||||
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`
|
||||
|
||||
|
|
|
@ -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} »"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue