Added MaximumQuantityElement and MinimumQuantityElement.

This commit is contained in:
Geoff Taylor 2021-11-18 12:51:57 +00:00
parent 06bd490652
commit e3f1939287
10 changed files with 416 additions and 3 deletions

33
.envrc
View File

@ -2,3 +2,36 @@ CURRENT_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
export PATH=$PATH:$CURRENT_DIRECTORY/bin:$CURRENT_DIRECTORY/scripts
# Automatically set up our virtual environment in .venv (same as poetry is configured to use).
# From: https://github.com/direnv/direnv/wiki/Python
realpath() {
[[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
}
layout_python-venv() {
local python=${1:-python3}
[[ $# -gt 0 ]] && shift
unset PYTHONHOME
if [[ -n $VIRTUAL_ENV ]]; then
VIRTUAL_ENV=$(realpath "${VIRTUAL_ENV}")
else
local python_version
python_version=$("$python" -c "import platform; print(platform.python_version())")
if [[ -z $python_version ]]; then
log_error "Could not detect Python version"
return 1
fi
VIRTUAL_ENV=$PWD/.direnv/python-venv-$python_version
fi
export VIRTUAL_ENV
if [[ ! -d $VIRTUAL_ENV ]]; then
log_status "no venv found; creating $VIRTUAL_ENV"
"$python" -m venv "$VIRTUAL_ENV"
fi
PATH="${VIRTUAL_ENV}/bin:${PATH}"
export PATH
}
export VIRTUAL_ENV=.venv
layout python-venv

View File

@ -237,6 +237,26 @@ The result is if you specify `--fixedspread-value 2 --fixedspread-value 4` the B
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.
### `MaximumQuantityElement`
> Specified using: `--chain maximumquantity`
> Accepts parameter: `--maximumquantity-size`
> Accepts parameter: `--maximumquantity-remove`
Ensures orders' quantities are always less than the maximum. Will either:
* Remove the order if the position size is too high, or
* Set the too-high position size to the permitted maximum
This `Element` examines every order to make sure the order quantity is less than or equal to the `--maximumquantity-size`. If it is less, this `Element` takes no further action on it.
If the order quantity is greater than `--maximumquantity-size`, then either:
1. (default) the order quantity will be reduced to the value specified by `--maximumquantity-size`.
2. (if `--maximumquantity-remove` is specified) the order will be removed from further processing and so will not be placed.
### `MinimumChargeElement`
> Specified using: `--chain minimumcharge`
@ -267,6 +287,26 @@ The result is if you specify `--minimumcharge-ratio 2 --minimumcharge-ratio 4` t
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.
### `MinimumQuantityElement`
> Specified using: `--chain minimumquantity`
> Accepts parameter: `--minimumquantity-size`
> Accepts parameter: `--minimumquantity-remove`
Ensures orders' quantities are always greater than the minimum. Will either:
* Remove the order if the position size is too low, or
* Set the too-low position size to the permitted minimum
This `Element` examines every order to make sure the order quantity is greater than or equal to the `--minimumquantity-size`. If it is greater, this `Element` takes no further action on it.
If the order quantity is less than `--minimumquantity-size`, then either:
1. (default) the order quantity will be increased to the value specified by `--minimumquantity-size`.
2. (if `--minimumquantity-remove` is specified) the order will be removed from further processing and so will not be placed.
### `PreventPostOnlyCrossingBookElement`
> Specified using `--chain preventpostonlycrossingbook`

View File

@ -14,7 +14,6 @@
# [Email](mailto:hello@blockworks.foundation)
import argparse
import mango
import typing

View File

@ -24,6 +24,8 @@ from .confidenceintervalelement import ConfidenceIntervalElement
from .element import Element
from .fixedpositionsizeelement import FixedPositionSizeElement
from .fixedspreadelement import FixedSpreadElement
from .maximumquantityelement import MaximumQuantityElement
from .minimumquantityelement import MinimumQuantityElement
from .minimumchargeelement import MinimumChargeElement
from .preventpostonlycrossingbookelement import PreventPostOnlyCrossingBookElement
from .quotesinglesideelement import QuoteSingleSideElement
@ -59,6 +61,8 @@ class ChainBuilder:
ConfidenceIntervalElement.add_command_line_parameters(parser)
FixedSpreadElement.add_command_line_parameters(parser)
FixedPositionSizeElement.add_command_line_parameters(parser)
MaximumQuantityElement.add_command_line_parameters(parser)
MinimumQuantityElement.add_command_line_parameters(parser)
MinimumChargeElement.add_command_line_parameters(parser)
PreventPostOnlyCrossingBookElement.add_command_line_parameters(parser)
QuoteSingleSideElement.add_command_line_parameters(parser)
@ -100,8 +104,12 @@ class ChainBuilder:
return FixedSpreadElement.from_command_line_parameters(args)
elif proper_name == "FIXEDPOSITIONSIZE":
return FixedPositionSizeElement.from_command_line_parameters(args)
elif proper_name == "MAXIMUMQUANTITY":
return MaximumQuantityElement.from_command_line_parameters(args)
elif proper_name == "MINIMUMCHARGE":
return MinimumChargeElement.from_command_line_parameters(args)
elif proper_name == "MINIMUMQUANTITY":
return MinimumQuantityElement.from_command_line_parameters(args)
elif proper_name == "PREVENTPOSTONLYCROSSINGBOOK":
return PreventPostOnlyCrossingBookElement.from_command_line_parameters(args)
elif proper_name == "QUOTESINGLESIDE":

View File

@ -0,0 +1,69 @@
# # ⚠ 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 argparse
import mango
import typing
from decimal import Decimal
from .element import Element
from ...modelstate import ModelState
# # 🥭 MaximumQuantityElement class
#
# Ensures orders' quantities are always less than the maximum. Will either:
# * Remove the order if the position size is too high, or
# * Set the too-high position size to the permitted maximum
#
class MaximumQuantityElement(Element):
def __init__(self, maximum_quantity: Decimal, remove: bool = False) -> None:
super().__init__()
self.maximum_quantity: Decimal = maximum_quantity
self.remove: bool = remove
@staticmethod
def add_command_line_parameters(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--maximumquantity-size", type=Decimal, default=Decimal(1),
help="the maximum permitted order quantity")
parser.add_argument("--maximumquantity-remove", action="store_true", default=False,
help="remove an order that has too big a quantity (default is to reduce order quantity to maximum)")
@staticmethod
def from_command_line_parameters(args: argparse.Namespace) -> "MaximumQuantityElement":
return MaximumQuantityElement(args.maximumquantity_size, args.maximumquantity_remove)
def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]:
new_orders: typing.List[mango.Order] = []
for order in orders:
if order.quantity < self.maximum_quantity:
new_orders += [order]
else:
if self.remove:
self.logger.debug(f"""Order change - order quantity is greater than maximum of {self.maximum_quantity} so removing:
Old: {order}
New: None""")
else:
new_order: mango.Order = order.with_quantity(self.maximum_quantity)
self.logger.debug(f"""Order change - order quantity is greater than maximum of {self.maximum_quantity} so changing order quantity to {self.maximum_quantity}:
Old: {order}
New: {new_order}""")
new_orders += [new_order]
return new_orders
def __str__(self) -> str:
return f"« 𝙼𝚊𝚡𝚒𝚖𝚞𝚖𝚀𝚞𝚊𝚗𝚝𝚒𝚝𝚢𝙴𝚕𝚎𝚖𝚎𝚗𝚝 [maximum quantity: {self.maximum_quantity}, remove: {self.remove}] »"

View File

@ -0,0 +1,69 @@
# # ⚠ 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 argparse
import mango
import typing
from decimal import Decimal
from .element import Element
from ...modelstate import ModelState
# # 🥭 MinimumQuantityElement class
#
# Ensures orders' quantities are always greater than the minimum. Will either:
# * Remove the order if the position size is too low, or
# * Set the too-low position size to the permitted minimum
#
class MinimumQuantityElement(Element):
def __init__(self, minimum_quantity: Decimal, remove: bool = False) -> None:
super().__init__()
self.minimum_quantity: Decimal = minimum_quantity
self.remove: bool = remove
@staticmethod
def add_command_line_parameters(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--minimumquantity-size", type=Decimal, default=Decimal(1),
help="the minimum permitted quantity")
parser.add_argument("--minimumquantity-remove", action="store_true", default=False,
help="remove an order that has too small a quantity (default is to increase order quantity to minimum)")
@staticmethod
def from_command_line_parameters(args: argparse.Namespace) -> "MinimumQuantityElement":
return MinimumQuantityElement(args.minimumquantity_size, args.minimumquantity_remove)
def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]:
new_orders: typing.List[mango.Order] = []
for order in orders:
if order.quantity > self.minimum_quantity:
new_orders += [order]
else:
if self.remove:
self.logger.debug(f"""Order change - order quantity is less than minimum of {self.minimum_quantity} so removing:
Old: {order}
New: None""")
else:
new_order: mango.Order = order.with_quantity(self.minimum_quantity)
self.logger.debug(f"""Order change - order quantity is less than minimum of {self.minimum_quantity} so changing order quantity to {self.minimum_quantity}:
Old: {order}
New: {new_order}""")
new_orders += [new_order]
return new_orders
def __str__(self) -> str:
return f"« 𝙼𝚒𝚗𝚒𝚖𝚞𝚖𝚀𝚞𝚊𝚗𝚝𝚒𝚝𝚢𝙴𝚕𝚎𝚖𝚎𝚗𝚝 [minimum quantity: {self.minimum_quantity}, remove: {self.remove}] »"

View File

@ -14,7 +14,6 @@
# [Email](mailto:hello@blockworks.foundation)
import argparse
import mango
import typing

View File

@ -0,0 +1,98 @@
import argparse
import typing
from ...context import mango
from ...fakes import fake_context, fake_model_state, fake_order
from decimal import Decimal
from mango.marketmaking.orderchain.maximumquantityelement import MaximumQuantityElement
bids: typing.Sequence[mango.Order] = [
fake_order(price=Decimal(78), quantity=Decimal(1), side=mango.Side.BUY),
fake_order(price=Decimal(77), quantity=Decimal(2), side=mango.Side.BUY),
fake_order(price=Decimal(76), quantity=Decimal(1), side=mango.Side.BUY),
fake_order(price=Decimal(75), quantity=Decimal(5), side=mango.Side.BUY),
fake_order(price=Decimal(74), quantity=Decimal(3), side=mango.Side.BUY),
fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY)
]
asks: typing.Sequence[mango.Order] = [
fake_order(price=Decimal(82), quantity=Decimal(3), side=mango.Side.SELL),
fake_order(price=Decimal(83), quantity=Decimal(1), side=mango.Side.SELL),
fake_order(price=Decimal(84), quantity=Decimal(1), side=mango.Side.SELL),
fake_order(price=Decimal(85), quantity=Decimal(3), side=mango.Side.SELL),
fake_order(price=Decimal(86), quantity=Decimal(3), side=mango.Side.SELL),
fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL)
]
orderbook: mango.OrderBook = mango.OrderBook("TEST", bids, asks)
model_state = fake_model_state(orderbook=orderbook)
def test_from_args() -> None:
args: argparse.Namespace = argparse.Namespace(
maximumquantity_size=Decimal(7),
maximumquantity_remove=True)
actual: MaximumQuantityElement = MaximumQuantityElement.from_command_line_parameters(args)
assert actual is not None
assert actual.maximum_quantity == 7
assert actual.remove
def test_low_buy_not_updated() -> None:
context = fake_context()
order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY)
actual: MaximumQuantityElement = MaximumQuantityElement(Decimal(10))
result = actual.process(context, model_state, [order])
assert result[0] == order
def test_high_buy_updated() -> None:
context = fake_context()
order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(17), side=mango.Side.BUY)
actual: MaximumQuantityElement = MaximumQuantityElement(Decimal(10))
result = actual.process(context, model_state, [order])
assert result[0].quantity == 10
def test_high_buy_removed() -> None:
context = fake_context()
order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(17), side=mango.Side.BUY)
actual: MaximumQuantityElement = MaximumQuantityElement(Decimal(10), True)
result = actual.process(context, model_state, [order])
assert len(result) == 0
def test_low_sell_not_updated() -> None:
context = fake_context()
order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL)
actual: MaximumQuantityElement = MaximumQuantityElement(Decimal(10))
result = actual.process(context, model_state, [order])
assert result[0] == order
def test_high_sell_updated() -> None:
context = fake_context()
order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(16), side=mango.Side.SELL)
actual: MaximumQuantityElement = MaximumQuantityElement(Decimal(10))
result = actual.process(context, model_state, [order])
assert result[0].quantity == 10
def test_high_sell_removed() -> None:
context = fake_context()
order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(16), side=mango.Side.SELL)
actual: MaximumQuantityElement = MaximumQuantityElement(Decimal(10), True)
result = actual.process(context, model_state, [order])
assert len(result) == 0

View File

@ -0,0 +1,98 @@
import argparse
import typing
from ...context import mango
from ...fakes import fake_context, fake_model_state, fake_order
from decimal import Decimal
from mango.marketmaking.orderchain.minimumquantityelement import MinimumQuantityElement
bids: typing.Sequence[mango.Order] = [
fake_order(price=Decimal(78), quantity=Decimal(1), side=mango.Side.BUY),
fake_order(price=Decimal(77), quantity=Decimal(2), side=mango.Side.BUY),
fake_order(price=Decimal(76), quantity=Decimal(1), side=mango.Side.BUY),
fake_order(price=Decimal(75), quantity=Decimal(5), side=mango.Side.BUY),
fake_order(price=Decimal(74), quantity=Decimal(3), side=mango.Side.BUY),
fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY)
]
asks: typing.Sequence[mango.Order] = [
fake_order(price=Decimal(82), quantity=Decimal(3), side=mango.Side.SELL),
fake_order(price=Decimal(83), quantity=Decimal(1), side=mango.Side.SELL),
fake_order(price=Decimal(84), quantity=Decimal(1), side=mango.Side.SELL),
fake_order(price=Decimal(85), quantity=Decimal(3), side=mango.Side.SELL),
fake_order(price=Decimal(86), quantity=Decimal(3), side=mango.Side.SELL),
fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL)
]
orderbook: mango.OrderBook = mango.OrderBook("TEST", bids, asks)
model_state = fake_model_state(orderbook=orderbook)
def test_from_args() -> None:
args: argparse.Namespace = argparse.Namespace(
minimumquantity_size=Decimal(7),
minimumquantity_remove=True)
actual: MinimumQuantityElement = MinimumQuantityElement.from_command_line_parameters(args)
assert actual is not None
assert actual.minimum_quantity == 7
assert actual.remove
def test_high_buy_not_updated() -> None:
context = fake_context()
order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(20), side=mango.Side.BUY)
actual: MinimumQuantityElement = MinimumQuantityElement(Decimal(10))
result = actual.process(context, model_state, [order])
assert result[0] == order
def test_low_buy_updated() -> None:
context = fake_context()
order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY)
actual: MinimumQuantityElement = MinimumQuantityElement(Decimal(10))
result = actual.process(context, model_state, [order])
assert result[0].quantity == 10
def test_low_buy_removed() -> None:
context = fake_context()
order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY)
actual: MinimumQuantityElement = MinimumQuantityElement(Decimal(10), True)
result = actual.process(context, model_state, [order])
assert len(result) == 0
def test_high_sell_not_updated() -> None:
context = fake_context()
order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(16), side=mango.Side.SELL)
actual: MinimumQuantityElement = MinimumQuantityElement(Decimal(10))
result = actual.process(context, model_state, [order])
assert result[0] == order
def test_low_sell_updated() -> None:
context = fake_context()
order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL)
actual: MinimumQuantityElement = MinimumQuantityElement(Decimal(10))
result = actual.process(context, model_state, [order])
assert result[0].quantity == 10
def test_low_sell_removed() -> None:
context = fake_context()
order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL)
actual: MinimumQuantityElement = MinimumQuantityElement(Decimal(10), True)
result = actual.process(context, model_state, [order])
assert len(result) == 0

View File

@ -55,7 +55,7 @@ def test_ask_price_updated() -> None:
assert result[0].price == 81
def test_accumulation_ignores_own_orders_updated() -> None:
def test_top_check_ignores_own_orders_updated() -> None:
order_owner: PublicKey = fake_seeded_public_key("order owner")
bids: typing.Sequence[mango.Order] = [
fake_order(price=Decimal(78), quantity=Decimal(1), side=mango.Side.BUY).with_owner(order_owner),