mango-explorer/docs/MarketmakingIntroduction.md

205 lines
13 KiB
Markdown
Raw Permalink Normal View History

# 🥭 Mango Explorer
2022-03-09 01:21:08 -08:00
# 🏛️ Marketmaking
Traders buy and sell, but it helps when there are reliable entities for them to trade against. And while an individual trader may buy or sell, they typically arent doing both at the same time on the same symbol. In contrast, a marketmaker places both buy and sell orders for the same symbol, producing a valuation of the symbol and saying how much theyd be willing to pay for some quantity, and how much theyd ask to part with some quantity. They _literally make a market_ by always providing a price at which someone can buy and a price at which someone can sell, and profit by the difference between the buy and sell prices - the spread.
How the marketmaker knows what prices to use, how much inventory to offer, and how to manage risk are all great questions that will not be adequately addressed here. Sorry. Successful marketmakers often guard their approaches and I just dont know any of their secrets.
Instead, lets look at the mechanics of marketmaking on 🥭 Mango.
# 📉 Worlds Simplest Marketmaker
Lets start with a really simple example. Heres an [actual marketmaker](https://github.com/blockworks-foundation/mango-explorer/blob/v3/scripts/worlds-simplest-market-maker) that will cancel any existing orders, look up the current price on a market, place a BUY order below that price and a SELL order above that price, then pause, then go back to the beginning:
```
#!/usr/bin/env bash
MARKET=${1:-BTC-PERP}
FIXED_POSITION_SIZE=${2:-0.01}
FIXED_SPREAD=${3:-100}
SLEEP_BETWEEN_ORDER_PLACES=${4:-60}
ORACLE_MARKET=${MARKET//\-PERP/\/USDC}
printf "Running on market %s with position size %f and prices +/- %f from current price\nPress Control+C to stop...\n" $MARKET $FIXED_POSITION_SIZE $FIXED_SPREAD
while :
do
cancel-my-orders --name "WSMM ${MARKET} (cancel)" --market $MARKET --log-level ERROR
2022-01-19 04:11:19 -08:00
CURRENT_PRICE=$(show-price --provider serum --market $ORACLE_MARKET --log-level ERROR --cluster-name mainnet | cut -d"'" -f 2 | sed 's/,//')
place-order --name "WSMM ${MARKET} (buy)" --market $MARKET --order-type LIMIT \
--log-level ERROR --side BUY --quantity $FIXED_POSITION_SIZE --price $(echo "$CURRENT_PRICE - $FIXED_SPREAD" | bc)
place-order --name "WSMM ${MARKET} (sell)" --market $MARKET --order-type LIMIT \
--log-level ERROR --side SELL --quantity $FIXED_POSITION_SIZE --price $(echo "$CURRENT_PRICE + $FIXED_SPREAD" | bc)
echo "Last ${MARKET} market-making action: $(date)" > /var/tmp/mango_healthcheck_worlds_simplest_market_maker
sleep $SLEEP_BETWEEN_ORDER_PLACES
done
```
You can run this and watch it place orders!
For example this will run it on the *ETH-PERP* market, placing a BUY at the current Serum price minus $10 and a SELL at the current Serum price plus $10, both with a position size of 1 ETH. It will then pause for 30 seconds before cancelling those orders (if they havent been filled) and placing fresh orders:
```
mango-explorer worlds-simplest-market-maker ETH-PERP 1 10 30
```
Thats not bad for 21 lines of `bash` scripting! OK, the price-fetching is a bit contorted, but you can see its calling:
* `cancel-my-orders`
2022-01-19 04:11:19 -08:00
* `show-price`
* `place-order` (BUY)
* `place-order` (SELL)
* `sleep`
# 📈 A Better Simple Marketmaker
There are many obvious problems with that approach so lets see if we can do better.
First of all lets write it in Python instead of `bash`, and lets put it in an object - `SimpleMarketMaker` - so that the methods can be overriddden allowing different functionality to be swapped in. Lets try to be a bit smarter about inventory. And lets add a check on orders to see if existing orders are OK - even though SOL is cheap theres no point wasting money cancelling and adding identical orders.
The [full class is available](https://github.com/blockworks-foundation/mango-explorer/blob/v3/mango/simplemarketmaking/simplemarketmaker.py), but the guts of it are in this looped section:
```
try:
# Update current state
price = self.oracle.fetch_price(self.context)
self._logger.info(f"Price is: {price}")
inventory = self.fetch_inventory()
# Calculate what we want the orders to be.
bid, ask = self.calculate_order_prices(price)
buy_quantity, sell_quantity = self.calculate_order_quantities(price, inventory)
current_orders = self.market_operations.load_my_orders(include_expired=True)
buy_orders = [order for order in current_orders if order.side == mango.Side.BUY]
if self.orders_require_action(buy_orders, bid, buy_quantity):
self._logger.info("Cancelling BUY orders.")
for order in buy_orders:
self.market_operations.cancel_order(order)
buy_order: mango.Order = mango.Order.from_basic_info(
mango.Side.BUY, bid, buy_quantity, mango.OrderType.POST_ONLY)
self.market_operations.place_order(buy_order)
sell_orders = [order for order in current_orders if order.side == mango.Side.SELL]
if self.orders_require_action(sell_orders, ask, sell_quantity):
self._logger.info("Cancelling SELL orders.")
for order in sell_orders:
self.market_operations.cancel_order(order)
sell_order: mango.Order = mango.Order.from_basic_info(
mango.Side.SELL, ask, sell_quantity, mango.OrderType.POST_ONLY)
self.market_operations.place_order(sell_order)
self.update_health_on_successful_iteration()
except Exception as exception:
self._logger.warning(
f"Pausing and continuing after problem running market-making iteration: {exception} - {traceback.format_exc()}")
# Wait and hope for fills.
self._logger.info(f"Pausing for {self.pause} seconds.")
time.sleep(self.pause.seconds)
```
Its following these steps:
* Fetch the current price
* Fetch the current inventory
* Calculate the desired price
* calculate the desired order size
* Fetch the marketmakers current orders
* If the desired BUY orders and existing orders dont match, cancel and replace them
* If the desired SELL orders and existing orders dont match, cancel and replace them
* Pause
You can see this is similar to the steps in the Worlds Simplest Marketmaker (above), but its a bit more complete. Instead of using a fixed position size, it varies it based on inventory. Instead of blindly cancelling orders, it checks to see if the current orders are what it wants them to be.
# 🍳 A Tangent On Market Operations
Its worth highlighting the use of a `MarketOperations` object in the `SimpleMarketMaker`. Lines like:
```
self.market_operations.place_order(buy_order)
```
show a simple interface to market actions that makes for nice, readable code.
What it hides, though, is that the marketmaker can work with 3 different market types:
* Serum
* Mango Spot
* Mango Perp
The `market_operations` object is loaded based on the desired market, so it doesnt matter (much) to the marketmaker if the market is Spot or Serum, it still follows the same steps and the `market_operations` takes action on the right market using the right instructions.
Behind the scenes, a similar variance happens with `MarketInstructions`. The actual instructions sent to Solana vary significantly depending on market type, but by having a unified `MarketInstructions` interface those differences can be largely hidden from marketmaking code. (Its not perfect but this commonality does help in most situations.)
This can serve as a kind of a Rosetta Stone for Mango. If you know and understand the instructions sent to Serum to place orders, cancel them, or crank the market, you can look at `SerumMarketInstructions` to see how those instructions are implemented in 🥭 Mango Explorer. Then you can compare that file with `SpotMarketInstructions` to see what bits are different for Spot markets (that require Mango Accounts) and what bits are similar. And then you can explore `PerpMarketInstructions` to see how those same actions are performed on perp markets.
# 🚀 A More Complete Marketmaker
Weve seen a common structure in the previous marketmakers, so lets see if we can provide a nice, common approach for actual marketmaking that allows people to write their own strategies for the interesting bits but that has most of the required code already in place.
The main design ideas behind the design are:
* every interval, a pulse is sent to run the marketmaker code
* the marketmaker is provided with relevant live data (like balances) but can fetch whatever other information it requires
* the main pluggable component is a desired orders builder. It looks at the state of balances, market, or other data sources, and it provides a list of BUY and SELL orders it would like to see on the orderbook.
* another component (also pluggable) compares the desired orders with any existing orders, and decides which orders need to be placed or cancelled.
Live data is provided as a `ModelState` parameter to the `pulse()` method, and its kept live by polling or a websocket connection that watches for changes in the underlying accounts. That doesnt matter (much) to the marketmaker code, it can just assume the `ModelState` parameter provides up-to-date information on balances, group, prices etc.
The `pulse()` method is called, say, every 30 seconds (again, its configurable). The current version of it looks like this:
```
def pulse(self, context: mango.Context, model_state: ModelState):
try:
payer = mango.CombinableInstructions.from_wallet(self.wallet)
desired_orders = self.desired_orders_builder.build(context, model_state)
existing_orders = self.order_tracker.existing_orders(model_state)
reconciled = self.order_reconciler.reconcile(model_state, existing_orders, desired_orders)
cancellations = mango.CombinableInstructions.empty()
for to_cancel in reconciled.to_cancel:
self._logger.info(f"Cancelling {self.market.symbol} {to_cancel}")
cancel = self.market_instruction_builder.build_cancel_order_instructions(to_cancel, ok_if_missing=True)
cancellations += cancel
place_orders = mango.CombinableInstructions.empty()
for to_place in reconciled.to_place:
desired_client_id: int = context.generate_client_id()
to_place_with_client_id = to_place.with_client_id(desired_client_id)
self.order_tracker.track(to_place_with_client_id)
self._logger.info(f"Placing {self.market.symbol} {to_place_with_client_id}")
place_order = self.market_instruction_builder.build_place_order_instructions(to_place_with_client_id)
place_orders += place_order
crank = self.market_instruction_builder.build_crank_instructions([])
settle = self.market_instruction_builder.build_settle_instructions()
(payer + cancellations + place_orders + crank + settle).execute(context, on_exception_continue=True)
self.pulse_complete.on_next(mango.local_now())
except Exception as exception:
self._logger.error(f"[{context.name}] Market-maker error on pulse: {exception} - {traceback.format_exc()}")
self.pulse_error.on_next(exception)
```
Again you can see the same steps:
* Build a list of desired orders
* Get the existing orders
* Compare them and decide what orders to place
Whats different here is:
* Desired orders are built using a `DesiredOrdersBuilder` object, and most people will probably want to provide their own version with their own strategy.
* Existing orders are tracked, rather than having to be fetched.
* Desired and existing orders are compared using an `OrderReconciler`. The default version takes a `tolerance` value and if an existing order has the same side (BUY or SELL) and both price and quantity are within the `tolerance` of a desired order, the existing order remains on the orderbook and the desired order is ignored.
* The code builds a list of instructions, and theyre executed in one step. This is faster, more efficient, and can allow cancels and places to happen in the same transaction. (Instruction szie can mean this doesnt happen though, but the `execute()` method takes this into account and uses as many transactions as necessary.)
You can see the different parameters the marketmaker takes by running:
```
mango-explorer marketmaker --help
```
You can run a basic instance of the marketmaker against the BTC-PERP market using [Pyth](https://pyth.network/) with:
```
2021-08-19 14:23:19 -07:00
mango-explorer marketmaker --market BTC/USDC --oracle-provider pyth-mainnet --position-size-ratio 0.01
```
# ⏭️ Next Steps
We started by saying what prices to use, how much inventory to offer, and how to manage risk are all great questions that will not be adequately addressed here.
Theyre up to you.
For now the code is in the [Mango Explorer repo](https://github.com/blockworks-foundation/mango-explorer/tree/v3). Happy marketmaking!