Moved marketmaker to an 'orderchain' model.

This commit is contained in:
Geoff Taylor 2021-08-22 19:48:20 +01:00
parent b56d114a4a
commit 3419c89033
24 changed files with 469 additions and 179 deletions

View File

@ -15,25 +15,18 @@ sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
import mango.marketmaking # nopep8
from mango.marketmaking.orderchain import chain # nopep8
from mango.marketmaking.orderchain import chainbuilder # nopep8
parser = argparse.ArgumentParser(description="Shows the on-chain data of a particular account.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
chainbuilder.ChainBuilder.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol to make market upon (e.g. ETH/USDC)")
parser.add_argument("--oracle-provider", type=str, required=True,
help="name of the price provider to use (e.g. pyth)")
parser.add_argument("--position-size-ratio", type=Decimal, required=True,
help="fraction of the token inventory to be bought or sold in each order")
parser.add_argument("--quote-position-bias", type=Decimal, default=Decimal(0),
help="bias to apply to quotes based on inventory position")
parser.add_argument("--existing-order-tolerance", type=Decimal, default=Decimal("0.001"),
help="tolerance in price and quantity when matching existing orders or cancelling/replacing")
parser.add_argument("--minimum-charge-ratio", type=Decimal, default=Decimal("0.0005"),
help="minimum fraction of the price to be accept as a spread")
parser.add_argument("--confidence-interval-level", type=Decimal, action="append",
help="the levels of weighting to apply to the confidence interval from the oracle: e.g. 1 - use the oracle confidence interval as the spread, 2 (risk averse, default) - multiply the oracle confidence interval by 2 to get the spread, 0.5 (aggressive) halve the oracle confidence interval to get the spread (can be specified multiple times to give multiple levels)")
parser.add_argument("--order-type", type=mango.OrderType, default=mango.OrderType.POST_ONLY,
choices=list(mango.OrderType), help="Order type: LIMIT, IOC or POST_ONLY")
parser.add_argument("--pulse-interval", type=int, default=10,
help="number of seconds between each 'pulse' of the market maker")
parser.add_argument("--account-index", type=int, default=0,
@ -130,13 +123,10 @@ manager.open()
order_reconciler = mango.marketmaking.ToleranceOrderReconciler(
args.existing_order_tolerance, args.existing_order_tolerance)
confidence_interval_levels = args.confidence_interval_level
if len(confidence_interval_levels) == 0:
confidence_interval_levels = [Decimal(2)]
desired_orders_builder = mango.marketmaking.ConfidenceIntervalDesiredOrdersBuilder(
args.position_size_ratio, args.minimum_charge_ratio, confidence_interval_levels, args.order_type, args.quote_position_bias)
desired_orders_chain: chain.Chain = chainbuilder.ChainBuilder.from_command_line_parameters(args)
market_maker = mango.marketmaking.MarketMaker(
wallet, market, market_instruction_builder, desired_orders_builder, order_reconciler)
wallet, market, market_instruction_builder, desired_orders_chain, order_reconciler)
model_state = mango.marketmaking.ModelState(market, latest_account_observer,
latest_group_observer, latest_price_observer,
latest_open_orders_observer, inventory_watcher,

View File

@ -14,11 +14,6 @@ from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
import mango.layouts # nopep8
import mango.marketmaking.fixedratiosdesiredordersbuilder # nopep8
import mango.marketmaking.marketmaker # nopep8
import mango.marketmaking.modelstate # nopep8
import mango.marketmaking.toleranceorderreconciler # nopep8
parser = argparse.ArgumentParser(description="Shows the on-chain data of a particular account.")
mango.ContextBuilder.add_command_line_parameters(parser)

View File

@ -17,11 +17,6 @@ from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
import mango.layouts # nopep8
import mango.marketmaking.fixedratiosdesiredordersbuilder # nopep8
import mango.marketmaking.marketmaker # nopep8
import mango.marketmaking.modelstate # nopep8
import mango.marketmaking.toleranceorderreconciler # nopep8
parser = argparse.ArgumentParser(
description="Watches one or many accounts (via a websocket) and sends a notification if the SOL balance falls below the --minimum-sol-balance threshold.")

View File

@ -120,3 +120,8 @@ logging.setLogRecordFactory(emojified_record_factory)
logging.basicConfig(level=logging.INFO,
datefmt="%Y-%m-%d %H:%M:%S",
format="%(asctime)s %(level_emoji)s %(name)-12.12s %(message)s")
# Stop libraries outputting lots of information unless it's a warning or worse.
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("solanaweb3").setLevel(logging.WARNING)

View File

@ -18,10 +18,9 @@ from decimal import Decimal
from .token import Token
# # 🥭 LotSizeConverter class
#
class LotSizeConverter():
def __init__(self, base: Token, base_lot_size: Decimal, quote: Token, quote_lot_size: Decimal):
self.base: Token = base
@ -29,6 +28,10 @@ class LotSizeConverter():
self.quote: Token = quote
self.quote_lot_size: Decimal = quote_lot_size
@property
def tick_size(self) -> Decimal:
return self.price_lots_to_value(Decimal(1))
def price_lots_to_native(self, price_lots: Decimal) -> Decimal:
return (price_lots * self.quote_lot_size) / self.base_lot_size
@ -52,8 +55,6 @@ class LotSizeConverter():
# # 🥭 NullLotSizeConverter class
#
class NullLotSizeConverter(LotSizeConverter):
def __init__(self):
super().__init__(None, Decimal(1), None, Decimal(1))
@ -72,3 +73,29 @@ class NullLotSizeConverter(LotSizeConverter):
def __str__(self) -> str:
return "« 𝙽𝚞𝚕𝚕𝙻𝚘𝚝𝚂𝚒𝚣𝚎𝙲𝚘𝚗𝚟𝚎𝚛𝚝𝚎𝚛 »"
# # 🥭 RaisingLotSizeConverter class
#
class RaisingLotSizeConverter(LotSizeConverter):
def __init__(self):
super().__init__(None, Decimal(-1), None, Decimal(-1))
def price_lots_to_native(self, price_lots: Decimal) -> Decimal:
raise NotImplementedError(
"RaisingLotSizeConverter.price_lots_to_native() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called.")
def quantity_lots_to_native(self, quantity_lots: Decimal) -> Decimal:
raise NotImplementedError(
"RaisingLotSizeConverter.quantity_lots_to_native() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called.")
def price_lots_to_value(self, price_lots: Decimal) -> Decimal:
raise NotImplementedError(
"RaisingLotSizeConverter.price_lots_to_value() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called.")
def quantity_lots_to_value(self, quantity_lots: Decimal) -> Decimal:
raise NotImplementedError(
"RaisingLotSizeConverter.quantity_lots_to_value() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called.")
def __str__(self) -> str:
return "« 𝙽𝚞𝚕𝚕𝙻𝚘𝚝𝚂𝚒𝚣𝚎𝙲𝚘𝚗𝚟𝚎𝚛𝚝𝚎𝚛 »"

View File

@ -20,6 +20,7 @@ import logging
from solana.publickey import PublicKey
from .lotsizeconverter import LotSizeConverter
from .token import Token
@ -40,13 +41,14 @@ class InventorySource(enum.Enum):
#
class Market(metaclass=abc.ABCMeta):
def __init__(self, program_id: PublicKey, address: PublicKey, inventory_source: InventorySource, base: Token, quote: Token):
def __init__(self, program_id: PublicKey, address: PublicKey, inventory_source: InventorySource, base: Token, quote: Token, lot_size_converter: LotSizeConverter):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.program_id: PublicKey = program_id
self.address: PublicKey = address
self.inventory_source: InventorySource = inventory_source
self.base: Token = base
self.quote: Token = quote
self.lot_size_converter: LotSizeConverter = lot_size_converter
@property
def symbol(self) -> str:

View File

@ -1,6 +1,3 @@
from .confidenceintervaldesiredordersbuilder import ConfidenceIntervalDesiredOrdersBuilder
from .desiredordersbuilder import DesiredOrdersBuilder, NullDesiredOrdersBuilder
from .fixedratiosdesiredordersbuilder import FixedRatiosDesiredOrdersBuilder
from .marketmaker import MarketMaker
from .modelstate import ModelState
from .orderreconciler import OrderReconciler, NullOrderReconciler

View File

@ -1,82 +0,0 @@
# # ⚠ 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 mango
import typing
from decimal import Decimal
from .desiredordersbuilder import DesiredOrdersBuilder
from .modelstate import ModelState
# # 🥭 ConfidenceIntervalDesiredOrdersBuilder class
#
# Builds orders using a fixed position size ratio but with a spread based on the confidence in the oracle price.
#
class ConfidenceIntervalDesiredOrdersBuilder(DesiredOrdersBuilder):
def __init__(self, position_size_ratio: Decimal, min_price_ratio: Decimal, confidence_interval_levels: typing.Sequence[Decimal] = [Decimal(2)], order_type: mango.OrderType = mango.OrderType.POST_ONLY, quote_position_bias: Decimal = Decimal(0)):
super().__init__()
self.position_size_ratio: Decimal = position_size_ratio
self.min_price_ratio: Decimal = min_price_ratio
self.confidence_interval_levels: typing.Sequence[Decimal] = confidence_interval_levels
self.order_type: mango.OrderType = order_type
self.quote_position_bias: Decimal = quote_position_bias
def build(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[mango.Order]:
price: mango.Price = model_state.price
if price.source.supports & mango.SupportedOracleFeature.CONFIDENCE == 0:
raise Exception(f"Price does not support confidence interval: {price}")
base_tokens: mango.TokenValue = model_state.inventory.base
quote_tokens: mango.TokenValue = model_state.inventory.quote
total = (base_tokens.value * price.mid_price) + quote_tokens.value
quote_value_to_risk = total * self.position_size_ratio
position_size = quote_value_to_risk / price.mid_price
orders: typing.List[mango.Order] = []
# From Daffy on 20th August 2021:
# Formula to adjust price might look like this `pyth_price * (1 + (curr_pos / size) * pos_lean)`
# where pos_lean is a negative number
#
# size is the standard size you're quoting which I believe comes from the position-size-ratio
#
# So if my standard size I'm quoting is 0.0002 BTC, my current position is +0.0010 BTC, and pos_lean
# is -0.0001, you would move your quotes down by 0.0005 (or 5bps)
# (Private chat link: https://discord.com/channels/@me/832570058861314048/878343278523723787)
quote_position_bias = self.quote_position_bias * -1
bias = (1 + (model_state.inventory.base.value / position_size) * quote_position_bias)
for confidence_interval_level in self.confidence_interval_levels:
# From Daffy on 26th July 2021: max(pyth_conf * 2, price * min_charge)
# (Private chat link: https://discord.com/channels/@me/832570058861314048/869208592648134666)
charge = max(price.confidence * confidence_interval_level, price.mid_price * self.min_price_ratio)
bid: Decimal = (price.mid_price - charge) * bias
ask: Decimal = (price.mid_price + charge) * bias
orders += [
mango.Order.from_basic_info(mango.Side.BUY, price=bid,
quantity=position_size, order_type=self.order_type),
mango.Order.from_basic_info(mango.Side.SELL, price=ask,
quantity=position_size, order_type=self.order_type)
]
return orders
def __str__(self) -> str:
return f"« 𝙲𝚘𝚗𝚏𝚒𝚍𝚎𝚗𝚌𝚎𝙸𝚗𝚝𝚎𝚛𝚟𝚊𝚕𝙳𝚎𝚜𝚒𝚛𝚎𝚍𝙾𝚛𝚍𝚎𝚛𝚜𝙱𝚞𝚒𝚕𝚍𝚎𝚛 {self.order_type} - position size: {self.position_size_ratio}, min charge: {self.min_price_ratio}, confidence interval levels: {self.confidence_interval_levels} »"

View File

@ -21,28 +21,26 @@ import typing
from datetime import datetime
from .desiredordersbuilder import DesiredOrdersBuilder
from .modelstate import ModelState
from ..observables import EventSource
from .orderreconciler import OrderReconciler
from .ordertracker import OrderTracker
from .orderchain.chain import Chain
# # 🥭 MarketMaker class
#
# An event-driven market-maker.
#
class MarketMaker:
def __init__(self, wallet: mango.Wallet, market: mango.Market,
market_instruction_builder: mango.MarketInstructionBuilder,
desired_orders_builder: DesiredOrdersBuilder,
order_reconciler: OrderReconciler):
desired_orders_chain: Chain, order_reconciler: OrderReconciler):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.wallet: mango.Wallet = wallet
self.market: mango.Market = market
self.market_instruction_builder: mango.MarketInstructionBuilder = market_instruction_builder
self.desired_orders_builder: DesiredOrdersBuilder = desired_orders_builder
self.desired_orders_chain: Chain = desired_orders_chain
self.order_reconciler: OrderReconciler = order_reconciler
self.order_tracker: OrderTracker = OrderTracker()
@ -56,7 +54,7 @@ class MarketMaker:
try:
payer = mango.CombinableInstructions.from_wallet(self.wallet)
desired_orders = self.desired_orders_builder.build(context, model_state)
desired_orders = self.desired_orders_chain.process(context, model_state)
existing_orders = self.order_tracker.existing_orders(model_state)
reconciled = self.order_reconciler.reconcile(model_state, existing_orders, desired_orders)

View File

@ -18,6 +18,8 @@ import logging
import mango
import typing
from decimal import Decimal
# # 🥭 ModelState class
#
@ -73,6 +75,18 @@ class ModelState:
def asks(self) -> typing.Sequence[mango.Order]:
return self.asks_watcher.latest
@property
def top_bid(self) -> mango.Order:
return self.bids_watcher.latest[0]
@property
def top_ask(self) -> mango.Order:
return self.asks_watcher.latest[0]
@property
def spread(self) -> Decimal:
return self.top_ask.price - self.top_bid.price
@property
def existing_orders(self) -> typing.Sequence[mango.PlacedOrder]:
return self.placed_orders_container_watcher.latest.placed_orders

View File

@ -0,0 +1,64 @@
# # ⚠ 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 mango
import typing
from decimal import Decimal
from .element import Element
from ..modelstate import ModelState
# # 🥭 BiasQuoteOnPositionElement class
#
# Modifies an `Order`s price based on current inventory. Uses `quote_position_bias` to shift the price to sell
# more (if too much inventory) or buy more (if too little inventory).
#
class BiasQuoteOnPositionElement(Element):
def __init__(self, quote_position_bias: Decimal = Decimal(0)):
super().__init__()
self.quote_position_bias: Decimal = quote_position_bias
def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]:
if self.quote_position_bias == 0:
# Zero bias results in no changes to orders.
return orders
# From Daffy on 20th August 2021:
# Formula to adjust price might look like this `pyth_price * (1 + (curr_pos / size) * pos_lean)`
# where pos_lean is a negative number
#
# size is the standard size you're quoting which I believe comes from the position-size-ratio
#
# So if my standard size I'm quoting is 0.0002 BTC, my current position is +0.0010 BTC, and pos_lean
# is -0.0001, you would move your quotes down by 0.0005 (or 5bps)
# (Private chat link: https://discord.com/channels/@me/832570058861314048/878343278523723787)
quote_position_bias = self.quote_position_bias * -1
new_orders: typing.List[mango.Order] = []
for order in orders:
bias = (1 + (model_state.inventory.base.value / order.quantity) * quote_position_bias)
new_price: Decimal = order.price * bias
new_order: mango.Order = order.with_price(new_price)
self.logger.debug(f"""Order change - quote_position_bias {self.quote_position_bias} creates a bias factor of {bias}:
Old: {order}
New: {new_order}""")
new_orders += [new_order]
return new_orders
def __str__(self) -> str:
return f"« 𝙱𝚒𝚊𝚜𝚀𝚞𝚘𝚝𝚎𝙾𝚗𝙿𝚘𝚜𝚒𝚝𝚒𝚘𝚗𝙴𝚕𝚎𝚖𝚎𝚗𝚝 - bias: {self.quote_position_bias} »"

View File

@ -0,0 +1,48 @@
# # ⚠ 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 logging
import mango
import typing
from ..modelstate import ModelState
from .element import Element
# # 🥭 Chain class
#
# A `Chain` object takes a series of `Element`s and calls them in sequence to build a list of
# desired `Order`s.
#
# Only `Order`s returned from `process()` method are used as the final list of 'desired orders' for
# reconciling and possibly adding to the orderbook.
#
class Chain:
def __init__(self, elements: typing.Sequence[Element]):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.elements: typing.Sequence[Element] = elements
def process(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[mango.Order]:
orders: typing.Sequence[mango.Order] = []
for element in self.elements:
orders = element.process(context, model_state, orders)
return orders
def __repr__(self) -> str:
return f"{self}"
def __str__(self) -> str:
return f"""« 𝙲𝚑𝚊𝚒𝚗 of {len(self.elements)} elements »"""

View File

@ -0,0 +1,67 @@
# # ⚠ 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 typing
from decimal import Decimal
from ...orders import OrderType
from .biasquoteonpositionelement import BiasQuoteOnPositionElement
from .chain import Chain
from .confidenceintervalspreadelement import ConfidenceIntervalSpreadElement
from .element import Element
from .minimumchargeelement import MinimumChargeElement
from .preventpostonlycrossingbookelement import PreventPostOnlyCrossingBookElement
# # 🥭 ChainBuilder class
#
# A `ChainBuilder` class to allow building a `Chain`, keeping parameter and constructor complexities all in
# one place.
#
class ChainBuilder:
@staticmethod
def add_command_line_parameters(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--order-type", type=OrderType, default=OrderType.POST_ONLY,
choices=list(OrderType), help="Order type: LIMIT, IOC or POST_ONLY")
parser.add_argument("--position-size-ratio", type=Decimal, required=True,
help="fraction of the token inventory to be bought or sold in each order")
parser.add_argument("--confidence-interval-level", type=Decimal, action="append",
help="the levels of weighting to apply to the confidence interval from the oracle: e.g. 1 - use the oracle confidence interval as the spread, 2 (risk averse, default) - multiply the oracle confidence interval by 2 to get the spread, 0.5 (aggressive) halve the oracle confidence interval to get the spread (can be specified multiple times to give multiple levels)")
parser.add_argument("--quote-position-bias", type=Decimal, default=Decimal(0),
help="bias to apply to quotes based on inventory position")
parser.add_argument("--minimum-charge-ratio", type=Decimal, default=Decimal("0.0005"),
help="minimum fraction of the price to be accept as a spread")
# This function is the converse of `add_command_line_parameters()` - it takes
# an argument of parsed command-line parameters and expects to see the ones it added
# to that collection in the `add_command_line_parameters()` call.
#
# It then uses those parameters to create a properly-configured `Chain` object.
#
@staticmethod
def from_command_line_parameters(args: argparse.Namespace) -> Chain:
confidence_interval_levels: typing.Sequence[Decimal] = args.confidence_interval_level
if len(confidence_interval_levels) == 0:
confidence_interval_levels = [Decimal(2)]
elements: typing.List[Element] = [
ConfidenceIntervalSpreadElement(args.position_size_ratio, confidence_interval_levels, args.order_type),
BiasQuoteOnPositionElement(args.quote_position_bias),
MinimumChargeElement(args.minimum_charge_ratio),
PreventPostOnlyCrossingBookElement()
]
return Chain(elements)

View File

@ -0,0 +1,70 @@
# # ⚠ 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 mango
import typing
from decimal import Decimal
from .element import Element
from ..modelstate import ModelState
# # 🥭 ConfidenceIntervalSpreadElement class
#
# Ignores any input `Order`s (so probably best at the head of the chain). Builds orders using a fixed position
# size ratio but with a spread based on the confidence in the oracle price.
#
class ConfidenceIntervalSpreadElement(Element):
def __init__(self, position_size_ratio: Decimal, confidence_interval_levels: typing.Sequence[Decimal] = [Decimal(2)], order_type: mango.OrderType = mango.OrderType.POST_ONLY):
super().__init__()
self.position_size_ratio: Decimal = position_size_ratio
self.confidence_interval_levels: typing.Sequence[Decimal] = confidence_interval_levels
self.order_type: mango.OrderType = order_type
def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]:
price: mango.Price = model_state.price
if price.source.supports & mango.SupportedOracleFeature.CONFIDENCE == 0:
raise Exception(f"Price does not support confidence interval: {price}")
base_tokens: mango.TokenValue = model_state.inventory.base
quote_tokens: mango.TokenValue = model_state.inventory.quote
total = (base_tokens.value * price.mid_price) + quote_tokens.value
quote_value_to_risk = total * self.position_size_ratio
position_size = quote_value_to_risk / price.mid_price
new_orders: typing.List[mango.Order] = []
for confidence_interval_level in self.confidence_interval_levels:
charge = price.confidence * confidence_interval_level
bid: Decimal = price.mid_price - charge
ask: Decimal = price.mid_price + charge
new_orders += [
mango.Order.from_basic_info(mango.Side.BUY, price=bid,
quantity=position_size, order_type=self.order_type),
mango.Order.from_basic_info(mango.Side.SELL, price=ask,
quantity=position_size, order_type=self.order_type)
]
new_orders.sort(key=lambda ord: ord.price, reverse=True)
order_text = "\n ".join([f"{order}" for order in new_orders])
self.logger.debug(f"""Initial desired orders - spread {model_state.spread} ({model_state.top_bid.price} / {model_state.top_ask.price}):
{order_text}""")
return new_orders
def __str__(self) -> str:
return f"« 𝙲𝚘𝚗𝚏𝚒𝚍𝚎𝚗𝚌𝚎𝙸𝚗𝚝𝚎𝚛𝚟𝚊𝚕𝚂𝚙𝚛𝚎𝚊𝚍𝙴𝚕𝚎𝚖𝚎𝚗𝚝 {self.order_type} - position size: {self.position_size_ratio}, confidence interval levels: {self.confidence_interval_levels} »"

View File

@ -19,39 +19,26 @@ import logging
import mango
import typing
from .modelstate import ModelState
from ..modelstate import ModelState
# # 🥭 DesiredOrdersBuilder class
# # 🥭 Element class
#
# A builder that builds a list of orders we'd like to be on the orderbook.
# A base class for a part of a chain that can take in a sequence of elements and process them, changing
# them as desired.
#
# The logic of what orders to create will be implemented in a derived class.
# Only `Order`s returned from `process()` method are passed to the next element of the chain.
#
class DesiredOrdersBuilder(metaclass=abc.ABCMeta):
class Element(metaclass=abc.ABCMeta):
def __init__(self):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@abc.abstractmethod
def build(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[mango.Order]:
raise NotImplementedError("DesiredOrdersBuilder.build() is not implemented on the base type.")
def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]:
raise NotImplementedError("Element.process() is not implemented on the base type.")
def __repr__(self) -> str:
return f"{self}"
# # 🥭 NullDesiredOrdersBuilder class
#
# A no-op implementation of the `DesiredOrdersBuilder` that will never ask to create orders.
#
class NullDesiredOrdersBuilder(DesiredOrdersBuilder):
def __init__(self):
super().__init__()
def build(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[mango.Order]:
return []
def __str__(self) -> str:
return "« 𝙽𝚞𝚕𝚕𝙳𝚎𝚜𝚒𝚛𝚎𝚍𝙾𝚛𝚍𝚎𝚛𝚜𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »"
return """« 𝙴𝚕𝚎𝚖𝚎𝚗𝚝 »"""

View File

@ -20,16 +20,16 @@ import typing
from decimal import Decimal
from .desiredordersbuilder import DesiredOrdersBuilder
from .modelstate import ModelState
from .element import Element
from ..modelstate import ModelState
# # 🥭 FixedRatiosDesiredOrdersBuilder class
#
# Builds orders using a fixed spread ratio and a fixed position size ratio.
# Ignores any input `Order`s (so probably best at the head of the chain). Builds orders using a fixed spread
# ratio and a fixed position size ratio.
#
class FixedRatiosDesiredOrdersBuilder(DesiredOrdersBuilder):
class FixedRatiosElement(Element):
def __init__(self, spread_ratios: typing.Sequence[Decimal], position_size_ratios: typing.Sequence[Decimal], order_type: mango.OrderType = mango.OrderType.POST_ONLY):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
if len(spread_ratios) != len(position_size_ratios):
@ -39,14 +39,14 @@ class FixedRatiosDesiredOrdersBuilder(DesiredOrdersBuilder):
self.position_size_ratios: typing.Sequence[Decimal] = position_size_ratios
self.order_type: mango.OrderType = order_type
def build(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[mango.Order]:
def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]:
price: mango.Price = model_state.price
base_tokens: mango.TokenValue = model_state.inventory.base
quote_tokens: mango.TokenValue = model_state.inventory.quote
total = (base_tokens.value * price.mid_price) + quote_tokens.value
orders: typing.List[mango.Order] = []
new_orders: typing.List[mango.Order] = []
for counter in range(len(self.spread_ratios)):
position_size_ratio = self.position_size_ratios[counter]
quote_value_to_risk = total * position_size_ratio
@ -56,14 +56,14 @@ class FixedRatiosDesiredOrdersBuilder(DesiredOrdersBuilder):
bid: Decimal = price.mid_price - (price.mid_price * spread_ratio)
ask: Decimal = price.mid_price + (price.mid_price * spread_ratio)
orders += [
new_orders += [
mango.Order.from_basic_info(mango.Side.BUY, price=bid,
quantity=base_position_size, order_type=self.order_type),
mango.Order.from_basic_info(mango.Side.SELL, price=ask,
quantity=base_position_size, order_type=self.order_type)
]
return orders
return new_orders
def __str__(self) -> str:
return f"« 𝙵𝚒𝚡𝚎𝚍𝚁𝚊𝚝𝚒𝚘𝙳𝚎𝚜𝚒𝚛𝚎𝚍𝙾𝚛𝚍𝚎𝚛𝚜𝙱𝚞𝚒𝚕𝚍𝚎𝚛 using ratios - spread: {self.spread_ratios}, position size: {self.position_size_ratios} »"
return f"« 𝙵𝚒𝚡𝚎𝚍𝚁𝚊𝚝𝚒𝚘𝚜𝙴𝚕𝚎𝚖𝚎𝚗𝚝 using ratios - spread: {self.spread_ratios}, position size: {self.position_size_ratios} »"

View File

@ -0,0 +1,60 @@
# # ⚠ 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 mango
import typing
from decimal import Decimal
from .element import Element
from ..modelstate import ModelState
# # 🥭 MinimumChargeElement class
#
# May modifiy an `Order`s price if it would result in too small a difference from the mid-price, meaning less
# of a charge if that `Order` is filled.
#
class MinimumChargeElement(Element):
def __init__(self, minimum_charge_ratio: Decimal = Decimal(0)):
super().__init__()
self.minimum_charge_ratio: Decimal = minimum_charge_ratio
def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]:
# From Daffy on 26th July 2021: max(pyth_conf * 2, price * min_charge)
# (Private chat link: https://discord.com/channels/@me/832570058861314048/869208592648134666)
new_orders: typing.List[mango.Order] = []
for order in orders:
minimum_charge = model_state.price.mid_price * self.minimum_charge_ratio
current_charge = (model_state.price.mid_price - order.price).copy_abs()
if current_charge > minimum_charge:
# All OK with current order
new_orders += [order]
else:
if order.side == mango.Side.BUY:
new_price: Decimal = model_state.price.mid_price - minimum_charge
else:
new_price = model_state.price.mid_price + minimum_charge
new_order = order.with_price(new_price)
self.logger.debug(f"""Order change - price is less than minimum charge:
Old: {order}
New: {new_order}""")
new_orders += [new_order]
return new_orders
def __str__(self) -> str:
return f"« 𝙼𝚒𝚗𝚒𝚖𝚞𝚖𝙲𝚑𝚊𝚛𝚐𝚎𝙴𝚕𝚎𝚖𝚎𝚗𝚝 - minimum charge ratio: {self.minimum_charge_ratio} »"

View File

@ -0,0 +1,60 @@
# # ⚠ 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 mango
import typing
from decimal import Decimal
from .element import Element
from ..modelstate import ModelState
# # 🥭 PreventPostOnlyCrossingBookElement class
#
# May modifiy an `Order`s price if it would result in too small a difference from the mid-price, meaning less
# of a charge if that `Order` is filled.
#
class PreventPostOnlyCrossingBookElement(Element):
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.order_type == mango.OrderType.POST_ONLY:
if order.side == mango.Side.BUY and order.price >= model_state.top_bid.price:
new_buy_price: Decimal = model_state.top_bid.price - model_state.market.lot_size_converter.tick_size
new_buy: mango.Order = order.with_price(new_buy_price)
self.logger.debug(f"""Order change - would cross the orderbook {model_state.top_bid.price} / {model_state.top_ask.price}:
Old: {order}
New: {new_buy}""")
new_orders += [new_buy]
elif order.side == mango.Side.SELL and order.price <= model_state.top_ask.price:
new_sell_price: Decimal = model_state.top_ask.price + model_state.market.lot_size_converter.tick_size
new_sell: mango.Order = order.with_price(new_sell_price)
self.logger.debug(f"""Order change - would cross the orderbook {model_state.top_bid.price} / {model_state.top_ask.price}:
Old: {order}
New: {new_sell}""")
new_orders += [new_sell]
else:
# All OK with current order
new_orders += [order]
else:
# Only change POST_ONLY orders.
new_orders += [order]
return new_orders
def __str__(self) -> str:
return "« 𝙿𝚛𝚎𝚟𝚎𝚗𝚝𝙿𝚘𝚜𝚝𝙾𝚗𝚕𝚢𝙲𝚛𝚘𝚜𝚜𝚒𝚗𝚐𝙱𝚘𝚘𝚔𝙴𝚕𝚎𝚖𝚎𝚗𝚝 »"

View File

@ -39,7 +39,6 @@ from .liquidationevent import LiquidationEvent
#
# Derived classes should not override `send()` since that is the interface outside classes call and it's used to ensure `NotificationTarget`s don't throw an exception when sending.
#
class NotificationTarget(metaclass=abc.ABCMeta):
def __init__(self):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@ -76,8 +75,6 @@ class NotificationTarget(metaclass=abc.ABCMeta):
#
# The [Telegram instructions to create a bot](https://core.telegram.org/bots#creating-a-new-bot)
# show you how to create the bot token.
class TelegramNotificationTarget(NotificationTarget):
def __init__(self, address):
super().__init__()
@ -99,8 +96,6 @@ class TelegramNotificationTarget(NotificationTarget):
#
# The `DiscordNotificationTarget` sends messages to Discord.
#
class DiscordNotificationTarget(NotificationTarget):
def __init__(self, address):
super().__init__()
@ -168,8 +163,6 @@ class DiscordNotificationTarget(NotificationTarget):
# ]
# }'
# ```
class MailjetNotificationTarget(NotificationTarget):
def __init__(self, encoded_parameters):
super().__init__()
@ -224,8 +217,6 @@ class MailjetNotificationTarget(NotificationTarget):
# columns to the output. Token changes may arrive in different orders, so ordering of token
# changes is not guaranteed to be consistent from transaction to transaction.
#
class CsvFileNotificationTarget(NotificationTarget):
def __init__(self, filename):
super().__init__()
@ -257,8 +248,6 @@ class CsvFileNotificationTarget(NotificationTarget):
# This class takes a `NotificationTarget` and a filter function, and only calls the
# `NotificationTarget` if the filter function returns `True` for the notification item.
#
class FilteringNotificationTarget(NotificationTarget):
def __init__(self, inner_notifier: NotificationTarget, filter_func: typing.Callable[[typing.Any], bool]):
super().__init__()
@ -279,17 +268,12 @@ class FilteringNotificationTarget(NotificationTarget):
# `NotificationTarget` to be plugged in to the `logging` subsystem to receive log messages
# and notify however it chooses.
#
class NotificationHandler(logging.StreamHandler):
def __init__(self, target: NotificationTarget):
logging.StreamHandler.__init__(self)
self.target = target
def emit(self, record):
# Don't send error logging from solanaweb3
if record.name == "solanaweb3.rpc.httprpc.HTTPClient":
return
message = self.format(record)
self.target.send_notification(message)
@ -302,8 +286,6 @@ class NotificationHandler(logging.StreamHandler):
# This is most likely used when parsing command-line arguments - this function can be used
# in the `type` parameter of an `add_argument()` call.
#
def parse_subscription_target(target):
protocol, destination = target.split(":", 1)

View File

@ -122,6 +122,11 @@ class Order(typing.NamedTuple):
return Order(id=self.id, side=self.side, price=self.price, quantity=self.quantity,
client_id=client_id, owner=self.owner, order_type=self.order_type)
# Returns an identical order with the price changed.
def with_price(self, price: Decimal) -> "Order":
return Order(id=self.id, side=self.side, price=price, quantity=self.quantity,
client_id=self.client_id, owner=self.owner, order_type=self.order_type)
@staticmethod
def from_serum_order(serum_order: PySerumOrder) -> "Order":
price = Decimal(serum_order.info.price)

View File

@ -20,7 +20,7 @@ from solana.publickey import PublicKey
from .accountinfo import AccountInfo
from .context import Context
from .group import Group
from .lotsizeconverter import LotSizeConverter
from .lotsizeconverter import LotSizeConverter, RaisingLotSizeConverter
from .market import Market, InventorySource
from .orderbookside import PerpOrderBookSide
from .orders import Order
@ -35,7 +35,7 @@ from .token import Token
#
class PerpMarket(Market):
def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, underlying_perp_market: PerpMarketDetails):
super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote)
super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote, RaisingLotSizeConverter())
self.underlying_perp_market: PerpMarketDetails = underlying_perp_market
self.lot_size_converter: LotSizeConverter = LotSizeConverter(
base, underlying_perp_market.base_lot_size, quote, underlying_perp_market.quote_lot_size)
@ -92,7 +92,7 @@ class PerpMarket(Market):
#
class PerpMarketStub(Market):
def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, group_address: PublicKey):
super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote)
super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote, RaisingLotSizeConverter())
self.group_address: PublicKey = group_address
def load(self, context: Context, group: typing.Optional[Group] = None) -> PerpMarket:

View File

@ -22,6 +22,7 @@ from solana.publickey import PublicKey
from .accountinfo import AccountInfo
from .context import Context
from .lotsizeconverter import LotSizeConverter, RaisingLotSizeConverter
from .market import Market, InventorySource
from .orders import Order
from .serumeventqueue import SerumEvent, SerumEventQueue
@ -34,8 +35,10 @@ from .token import Token
#
class SerumMarket(Market):
def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, underlying_serum_market: PySerumMarket):
super().__init__(program_id, address, InventorySource.SPL_TOKENS, base, quote)
super().__init__(program_id, address, InventorySource.SPL_TOKENS, base, quote, RaisingLotSizeConverter())
self.underlying_serum_market: PySerumMarket = underlying_serum_market
self.lot_size_converter: LotSizeConverter = LotSizeConverter(
base, underlying_serum_market.state.base_lot_size, quote, underlying_serum_market.state.quote_lot_size)
def unprocessed_events(self, context: Context) -> typing.Sequence[SerumEvent]:
event_queue: SerumEventQueue = SerumEventQueue.load(context, self.underlying_serum_market.state.event_queue())
@ -67,7 +70,7 @@ class SerumMarket(Market):
#
class SerumMarketStub(Market):
def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token):
super().__init__(program_id, address, InventorySource.SPL_TOKENS, base, quote)
super().__init__(program_id, address, InventorySource.SPL_TOKENS, base, quote, RaisingLotSizeConverter())
def load(self, context: Context) -> SerumMarket:
underlying_serum_market: PySerumMarket = PySerumMarket.load(

View File

@ -23,6 +23,7 @@ from solana.publickey import PublicKey
from .accountinfo import AccountInfo
from .context import Context
from .group import Group
from .lotsizeconverter import LotSizeConverter, RaisingLotSizeConverter
from .market import Market, InventorySource
from .orders import Order
from .serumeventqueue import SerumEvent, SerumEventQueue
@ -35,9 +36,11 @@ from .token import Token
#
class SpotMarket(Market):
def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, group: Group, underlying_serum_market: PySerumMarket):
super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote)
super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote, RaisingLotSizeConverter())
self.group: Group = group
self.underlying_serum_market: PySerumMarket = underlying_serum_market
self.lot_size_converter: LotSizeConverter = LotSizeConverter(
base, underlying_serum_market.state.base_lot_size, quote, underlying_serum_market.state.quote_lot_size)
def unprocessed_events(self, context: Context) -> typing.Sequence[SerumEvent]:
event_queue: SerumEventQueue = SerumEventQueue.load(context, self.underlying_serum_market.state.event_queue())
@ -71,7 +74,7 @@ class SpotMarket(Market):
class SpotMarketStub(Market):
def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, group_address: PublicKey):
super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote)
super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote, RaisingLotSizeConverter())
self.group_address: PublicKey = group_address
def load(self, context: Context, group: typing.Optional[Group]) -> SpotMarket: