From 9518d5e05d63345fb0da35c360c6df589d239952 Mon Sep 17 00:00:00 2001 From: Steve Flanagan Date: Wed, 27 Nov 2024 22:35:22 -0500 Subject: [PATCH 1/2] feat: market maker bot docs --- docs/apis/_category_.json | 5 + docs/apis/options-gateway/_category_.json | 5 + docs/apis/options-gateway/endpoints.md | 20 ++ .../options-gateway/introduction-and-setup.md | 174 ++++++++++++++++++ .../options-gateway/leveraging-the-gateway.md | 9 + 5 files changed, 213 insertions(+) create mode 100644 docs/apis/_category_.json create mode 100644 docs/apis/options-gateway/_category_.json create mode 100644 docs/apis/options-gateway/endpoints.md create mode 100644 docs/apis/options-gateway/introduction-and-setup.md create mode 100644 docs/apis/options-gateway/leveraging-the-gateway.md diff --git a/docs/apis/_category_.json b/docs/apis/_category_.json new file mode 100644 index 00000000..5b1ad374 --- /dev/null +++ b/docs/apis/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "APIs", + "position": 4.5, + "link": null +} diff --git a/docs/apis/options-gateway/_category_.json b/docs/apis/options-gateway/_category_.json new file mode 100644 index 00000000..792bd2bb --- /dev/null +++ b/docs/apis/options-gateway/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Options Gateway", + "position": 0, + "link": null +} diff --git a/docs/apis/options-gateway/endpoints.md b/docs/apis/options-gateway/endpoints.md new file mode 100644 index 00000000..4716a705 --- /dev/null +++ b/docs/apis/options-gateway/endpoints.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 2 +--- + +# Endpoints +Here are the endpoints we supply from the Options Gateway: + +TODO: note that the swagger docs also exist +--- +### localhost:8000/options/liquidity + +Supply these args: + +``` +{ + something: 1 +} +``` + +and get back these diff --git a/docs/apis/options-gateway/introduction-and-setup.md b/docs/apis/options-gateway/introduction-and-setup.md new file mode 100644 index 00000000..2482064c --- /dev/null +++ b/docs/apis/options-gateway/introduction-and-setup.md @@ -0,0 +1,174 @@ +--- +sidebar_position: 0 +--- + +# Introduction and Setup +Panoptic is a protocol that facilitates the trading of [Panoptions](/docs/terms/panoption), which are perpetual options instruments with fixed gamma between two prices that operate on a [streaming premium model](/docs/product/streamia). We have created an [Options Gateway API](https://github.com/panoptic-labs/options_gateway) that facilitates reading information about these markets, as well as managing positions in them. You can use this API to build market making bots, trading bots, liquidation bots, and more. + +We built our API as a fork of the [Hummingbot Gateway](https://github.com/hummingbot/gateway). It can be used in tandem with [Hummingbot](https://github.com/hummingbot/hummingbot) or with [our fork of Hummingbot](https://github.com/panoptic-labs/panoptic_hummingbot) that includes example Panoptic strategies. The API also functions [as a generic transaction-builder and chain-reader, decoupled from Hummingbot-specific functionality or custody setup](TODO: link to "As generic API"). + +--- +## Overview +We have added an `/options` namespace to the Hummingbot Gateway, and have included a number of ways to interact with Panoptic within that namespace: + +- You can call `mint` and `burn` with arbitrary token IDs to construct any Panoption possible. +- You can make arbitrary calls to the Panoptic subgraph, as well as convenience methods for getting your existing TokenIDs / positions. +- You can use analytical methods such as `delta` and `gamma` to power a strategy, with more to come. + +It also comes with Sepolia support out of the box, which is not found in upstream Hummingbot implementations. + +[Our Hummingbot fork](https://github.com/panoptic-labs/panoptic_hummingbot) includes example scripts showing how you could use this Gateway to run automated Panoptic trading strategies, such as market making. + +## Using the Gateway With Hummingbot +### Setting up +To run a Hummingbot script that interacts with Panoptic, you will need to set up [our Hummingbot fork](https://github.com/panoptic-labs/panoptic_hummingbot) and [the Gateway fork](https://github.com/panoptic-labs/options_gateway). + +#### Step 1: Setting up the hummingbot fork: + +1. First, clone the Hummingbot Fork repository down, and enter the directory we'll be working out of: + 1. `git clone https://github.com/panoptic-labs/panoptic_hummingbot.git` + 2. `cd panoptic_hummingbot` +2. Then, get the Hummingbot Anaconda environment ready: + 1. Ensure you have [Anaconda installed](https://www.anaconda.com/download) + 2. Then, simply use the installation shellscript: `./install` + 3. And finally, activate the newly installed environment: `conda activate hummingbot` +3. Next, you should be able to use the compilation shellscript to get your executables ready to run Hummingbot: + 1. `./compile` +4. And finally, you can pull up the terminal UI with the start shellscript: + 1. `./start` +5. The Hummingbot UI should now present itself, and you will be asked to set a passphrase. +6. The Hummingbot UI is split into two parts - the shell, on the left, where you can run commands such as starting strategy scripts, and the logs on the right. + +TODO: Insert your screenshot + +Your first shell command will be to generate some certificates to secure the data you supply your Hummingbot, such as the private key to a trading account. + + 1. Generate the certs by entering into the shell: `gateway generate-certs` + 2. You’ll be prompted to set a passphrase (And note that this is a different value than the password for your Hummingbot UI). + 3. Once you successfully generate the certificates, Hummingbot will list the directory those certificates are stored in. + 4. Copy the directory path it generates these certificates to - you'll need it for setting up the Gateway. + 5. Typically, this certificate destination is `~/panoptic_hummingbot/certs` - in other words, a certs sub-folder in the same folder as your hummingbot repo. + +You now have a Hummingbot set up and ready for your usage. Let's now give it a Gateway to interact with the blockchain through: + +#### Step 2: Setting up the gateway fork: + +1. Similarly, we will clone down the Gateway fork and enter the directory to work out of: + 1. `git clone git@github.com:panoptic-labs/options_gateway.git` + 2. `cd options_gateway` +2. We will then install our dependencies: + 1. `yarn` + 2. Ensure you’re using node 18.0.0 or higher - you can use [nvm](https://github.com/nvm-sh/nvm) to manage different node versions if needed. +3. Then, we compile the project and get a runnable Gateway executable: + 1. `yarn build` +4. Next, we run the built-in setup script: + 1. First, we give permissions to the script: `chmod a+x gateway-setup.sh` + 2. Then we run it: `./gateway-setup.sh` + 3. This script will prompt you to copy over the certificates from your Hummingbot (step 6.5 above). Enter the path (and possibly passphrase) for those certs. + +Your Gateway is now ready as well. Let's get these tools running: + +### Running + +1. Running the Gateway is quite easy - simply `yarn start` with the passphrase for your certificates from 6.5: `yarn start --passphrase=` +2. Similarly, you can run your Hummingbot with the `./start` command we mentioned above. Once you do this, if your Gateway is successfully running, you should see GATEWAY: ONLINE in the top of your log pane. +3. In the Hummingbot shell, you can explicitly connect to the Panoptic functionality in the Gateway by running `gateway connect panoptic`. This will prompt you to supply an RPC URL and a private key for a trading wallet. + 1. You can confirm that your connection was successful by running `gateway balance`. +4. To see the results of your Panoptic trading, you will likely want to track a number of tokens. You can add expand the tokens that `gateway balance` reports on via the `gateway connector-tokens panoptic_ethereum_sepolia` command (substituting `sepolia` for your target chain). + 1. Hummingbot will accept some well-known tokens out of the box by their ticker alone - so you can do things such as `gateway connector-tokens panoptic_ethereum_sepolia WETH`. + 2. And you can expand this list by modifying `options_gateway/src/templates/lists/erc20_tokens_sepolia.json` for your appropriate chain. + +Your Hummingbot can now interact with Panoptic. Let's write a script! + +### Scripting + +1. Your strategies will live in the `scripts` subfolder in the `panoptic_hummingbot` folder. Let's start an example script to operate on the Sepolia test market, T1/T0. Create a file called `panoptic_testing_example.py`. +2. For any strategies to trade successfully, you will have to approve the tokens from your trading wallet to the Panoptic and Uniswap contracts. The first thing your strategy should do is check if it has a sufficient allowance with the relevant Panoptic contracts: + 1. TODO list python command to call the gateway to query `.allowance` + 2. And if your allowance is insufficient, you should `.approve` the relevant Panoptic contracts: TODO list python command to call the gateway to `.approve`, and which specific Panoptic contracts you need to `.approve` / how to get their addresses. +3. Your strategy will also need to deposit collateral before proceeding to trade. + 1. TODO list command to query collateralTracker.balanceOf + 2. At the start of the strategy, we'll need to `.deposit`: TODO list command to call `.deposit` +4. After these initial approvals and deposits, we are ready to perform some regularly scheduled actions in an `on_tick` method + 1. This method gets called every `tick` of your Hummingbot config. By default, this is 1 second. TODO: List how to alter it to 12 seconds to match Ethereum block time. + 2. Depending on your trading philosophy, you may have different requirements on how much collateral you should have deposited at any one time. In general, you can use your account's current [buying power usage](TODO: Link to docs on what buying power usage is) as a threshold your strategy can manage. You can set some percentage that your buying power should be kept beneath, and `.deposit` as need be to top up on each tick. It'll work the same as our initial deposit in the setup: TODO list the command to deposit +4. With these basic building blocks, your strategy is ready to trade. You can now add more functionality into your `on_tick` to make trades - you can use the guide on [automatically selling straddles](/docs/apis/options-gateway/leveraging-the-gateway) as a starting point. + +## Using the Gateway As A Generic API +### Setting up + +You can use the Options Gateway as a generic transaction-building API without being tied into the full Hummingbot setup. This may be useful depending on your wallet management - using the Gateway alone will enable you to trade from multisigs or smart wallets, if you like, and still avoid the pain of constructing complex smart contract call sequences on your own. + +First, we'll need to set up the Gateway: + +1. Clone down the Gateway fork and enter the directory to work out of: + 1. `git clone git@github.com:panoptic-labs/options_gateway.git` + 2. `cd options_gateway` +2. Install the dependencies: + 1. `yarn` + 2. Ensure you’re using node 18.0.0 or higher - you can use [nvm](https://github.com/nvm-sh/nvm) to manage different node versions if needed. +3. Compile the project and get a runnable Gateway executable: + 1. `yarn build` +4. Now, we'll need to generate our certificates. In the full Hummingbot setup, you can do this from their terminal UI, but we'll do this using built-in Unix tools: + 1. Make a directory: `mkdir certificates` + 2. Then, generate the certificates using a self-signed CA: + 1. Create the CA: + 1. `openssl genrsa -out ./certificates/ca_key.pem 2048` + 2. `openssl req -new -x509 -key ./certificates/ca_key.pem -out ./certificates/ca_cert.pem -days 365 -subj "/C=US/ST=State/L=City/O=GatewayCA/CN=localhost"` + 2. Then create a server key and certificate signing request (CSR) + 1. `openssl genrsa -out ./certificates/server_key.pem 2048` + 2. `openssl req -new -key ./certificates/server_key.pem -out ./certificates/server.csr -subj "/C=US/ST=State/L=City/O=Gateway/CN=localhost"` + 3. Then, sign the server certificate with our CA: `openssl x509 -req -days 365 -in ./certificates/server.csr -CA ./certificates/ca_cert.pem -CAkey ./certificates/ca_key.pem -CAcreateserial -out ./certificates/server_cert.pem` + 1. Note that this expires after a year - you can increase this `-days` flag arbitrarily highly +5. Then, we'll run the gateway setup script, to initialise our Gateway config and copy over those certificates: + 1. Run the script: `./gateway-setup.sh` + 2. When prompted to copy over certificates, select Y and provide the path for the certificates you just created: `/path/to/your/options_gateway/certificates` + 3. And select Y to copy over the config. +6. And now, you should be able to run the gateway! There is a mandatory `passphrase` argument, but we won't be using it, so you can just run `yarn start --passphrase=anything` +7. Optionally, if you don't want to set up mutual TLS authentication, and allow your integration to communicate with the Gateway via `http` instead of `https`, you can change `conf/server.yml` `unsafeDevModeWithHTTP` flag to `true`. You'll get a security warning when you `yarn start` the Gateway, but if you have no sensitive information being passed between these two systems, it may make sense. + 1. If you skip this step, you'll need to use the CA you set up in step 4 above to generate some client-side keys, and pass those as headers from your integration. Request libraries like `axios` and `fetch` will provide helpers for using these SSL keys. +8. The Gateway is now running. We can send arbitrary requests to `localhost:15888` to generate transactions. + +Since we're only using it as a transaction generator, we'll be heavily leveraging the [doNotBroadcast parameter](TODO: LINK SECTION TALKING ABOUT THIS PARAM). When this parameter is `true`, the Panoptic endpoints will avoid trying to sign and broadcast transactions with the supplied `wallet`, but still allow you to use the unsignedTransactionPayload to sign and broadcast the transactions in whatever manner you like. + +To confirm functionality, let's generate an unsigned transaction to deposit some TokenRed tokens into the Sepolia test TokenRed<>TokenBlue market. + +First, ensure you have the Panoptic contracts approved to spend your TokenRed: TODO + +Then, you should be able to send a cURL request to generate this unsigned transaction payload for a deposit: + +``` +curl --header "Content-Type: application/json" \ + --request POST \ + --data '{ + "chain": "ethereum", + "network": "sepolia", + "connector": "panoptic", + "wallet": "0xYourWallet", + "collateralTracker": "0x203bc3e3A8031d4d71e79f95A51C89284F813c22", + "assets": "100000000000000000000", + "address": "0xYourWallet" + }' \ + http://localhost:15888/panoptic/deposit + ``` + + For testing, you can use Steve's wallet, 0x275e8f09f090cf9b8e77008643e33d477fbb05e6. That should get you a real, useable unsigned transaction payload that his wallet could sign, like so: + + ``` + (base) gateway_client_example:$ curl --header "Content-Type: application/json" --request POST --data '{ + "chain": "ethereum", + "network": "sepolia", + "connector": "panoptic", + "wallet": "0x275e8f09f090cf9b8e77008643e33d477fbb05e6", + "collateralTracker": "0x203bc3e3A8031d4d71e79f95A51C89284F813c22", + "assets": "100000000000000000000", + "address": "0x275e8f09f090cf9b8e77008643e33d477fbb05e6","doNotBroadcast": true + }' http://localhost:15888/options/deposit +{"tx":null,"network":"sepolia","timestamp":1732764503484,"unsignedTransaction":{"data":"0x6e553f650000000000000000000000000000000000000000000000056bc75e2d63100000000000000000000000000000275e8f09f090cf9b8e77008643e33d477fbb05e6","to":"0x203bc3e3A8031d4d71e79f95A51C89284F813c22","gasLimit":{"type":"BigNumber","hex":"0x01a97b"},"from":"0x275e8f09f090cf9b8e77008643e33d477fbb05e6"}} +``` + +### Integrating +// TODO: paste rust examples etc of sign-then-broadcast 2 liner from Slack + +## Contributing to the Gateway +TODO diff --git a/docs/apis/options-gateway/leveraging-the-gateway.md b/docs/apis/options-gateway/leveraging-the-gateway.md new file mode 100644 index 00000000..8669a24f --- /dev/null +++ b/docs/apis/options-gateway/leveraging-the-gateway.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 1 +--- + +# Leveraging the Gateway +With a functional gateway in hand, let's write a script to sell [straddles](/research/defi-option-straddle-101) at the current price. + +--- +### Step 1: From 0c8021f57e1172092dd369fb71528b0dc82965d2 Mon Sep 17 00:00:00 2001 From: Steve Flanagan Date: Mon, 2 Dec 2024 19:29:42 -0500 Subject: [PATCH 2/2] feat: walk through example strategy --- .../options-gateway/leveraging-the-gateway.md | 422 +++++++++++++++++- 1 file changed, 420 insertions(+), 2 deletions(-) diff --git a/docs/apis/options-gateway/leveraging-the-gateway.md b/docs/apis/options-gateway/leveraging-the-gateway.md index 8669a24f..def3ecf7 100644 --- a/docs/apis/options-gateway/leveraging-the-gateway.md +++ b/docs/apis/options-gateway/leveraging-the-gateway.md @@ -3,7 +3,425 @@ sidebar_position: 1 --- # Leveraging the Gateway -With a functional gateway in hand, let's write a script to sell [straddles](/research/defi-option-straddle-101) at the current price. +With a functional gateway in hand, let's write a script to sell [straddles](/research/defi-option-straddle-101) at the current price using the Panoptic Gateway. Straddles are a sell-side strategy where options are sold at the same strike price for both calls and puts, maximizing premium collection when the underlying remains close to the strike. We're going to plug the Hummingbot execution client into the Gateway to sell these around the current tick. + +Go [here](https://github.com/panoptic-labs/panoptic_hummingbot/pull/2/files#diff-b82cebb857e26f82f7199f157dd9fdbe9079c15e6d5faf6d8349413c6c4e2dd1) for the completed code. --- -### Step 1: +### Step 0: Get Hummingbot strategy boilerplate set up + +Hummingbot strategies have two high level concepts to be aware of: + +1. The `initialize` method: What should the bot do on startup? For example, you may need to set up allowances and deposit into protocols, or go acquire necessary data for your strategy like fetching pool addresses from a router. +2. The `on_tick` method: What logic should execute on the bot's configured "tick" frequency? (Note that in this context, tick means a unit of time, not a Uniswap tick). + +You can set up most strategies with these two methods and the necessary imports. Here's a boilerplate you can use for this tutorial below, which includes: + +- those two stubs +- some imports & helpers we'll use later +- our `initialize` method, which does things like fetch the pool addresses we need for this tutorial + +```python +import bisect +import numpy as np +import time +import asyncio + +from .utility.panoptic_helpers import utils as ph + +from hummingbot.client.settings import GatewayConnectionSetting +from hummingbot.core.gateway.gateway_http_client import GatewayHttpClient +from hummingbot.core.utils.async_utils import safe_ensure_future +from hummingbot.strategy.script_strategy_base import Decimal, ScriptStrategyBase + + +class TradePanoptions(ScriptStrategyBase): + # trading params and configuration + connector_chain_network = "panoptic_ethereum_sepolia" + trading_pair = {"t0-t1"} + markets = {} + perturbation_testing = True + verbosity = 1 + + launched = False # Have you launched the strategy? + initialized = False # Have all the initialization steps completed? + ready = True # Are all on-chain tasks complete and you're ready to process another one? + tick_count = 0 + + # executed each tick (configure tick size in Hummingbot client before launching strategy) + def on_tick(self): + # As written, the tick can now be equivalent to an actual clock, as processes are now controlled by + # flags and wrapped in safe_ensure_future(...) + self.log(f"Tick count: {self.tick_count}", 1) + self.tick_count = self.tick_count + 1 + + # initial setup - only execute once + if not self.launched: + self.log(f"Launching...", 1) + self.launched = True + safe_ensure_future(self.initialize()) + + # repeat each tick + if (self.initialized and self.ready): + # Tricky because the 'safe_ensure_future' bit immediately returns the contained logic as being complete. + # It won't wait for the processes inside to finish before allowing tick to tok. Can try to get around + # this by using flags, but that feels clunky. + self.ready=False + safe_ensure_future(self.monitor_and_apply_logic()) + + # async task since we are using Gateway + async def monitor_and_apply_logic(self): + # TODO: Fill in the interesting logic. + self.ready=True + + async def initialize(self): + self.t0_symbol, self.t1_symbol = list(self.trading_pair)[0].split("-") + self.connector, self.chain, self.network = self.connector_chain_network.split("_") + + # fetch wallet address and print balances + self.gateway_connections_conf = GatewayConnectionSetting.load() + if len(self.gateway_connections_conf) < 1: + self.log("No existing wallet.\n", 0) + return + self.wallet = [w for w in self.gateway_connections_conf if w["chain"] == self.chain and w["connector"] == self.connector and w["network"] == self.network] + self.address = self.wallet[0]['wallet_address'] + + self.request_payload = { + "chain": self.chain, + "network": self.network, + "connector": self.connector, + "address": self.address + } + + self.log(f"Getting token addresses...", 2) + self.log(f"POST /options/getTokenAddress [ connector: {self.connector}]", 0) + self.request_payload["tokenSymbol"]= self.t0_symbol + self.log(f"Finding token {self.t0_symbol}", 2) + response = await GatewayHttpClient.get_instance().api_request( + method="post", + path_url="options/getTokenAddress", + params=self.request_payload, + fail_silently=False + ) + + self.request_payload.update({ + "t0_address": response['tokenAddress'], + "token0Decimals": response['tokenDecimals'] + }) + self.log(f"t0 address: {self.request_payload['t0_address']}", 2) + + self.request_payload.update({ + "tokenSymbol": self.t1_symbol + }) + self.log(f"Finding token {self.t1_symbol}", 2) + response = await GatewayHttpClient.get_instance().api_request( + method="post", + path_url="options/getTokenAddress", + params=self.request_payload, + fail_silently=False + ) + self.request_payload.update({ + "t1_address": response['tokenAddress'], + "token1Decimals": response['tokenDecimals'] + }) + self.log(f"t1 address: {self.request_payload['t1_address']}", 2) + + self.log(f"Getting UniswapV3 token pool address...", 2) + self.log(f"POST /options/checkUniswapPool [ connector: {self.connector}]", 0) + + self.request_payload.update({ + "fee": 500 + }) + response = await GatewayHttpClient.get_instance().api_request( + method="post", + path_url="options/checkUniswapPool", + params=self.request_payload, + fail_silently=False + ) + self.request_payload.update({ + "uniswapV3PoolAddress": response["uniswapV3PoolAddress"] + }) + self.log(f"Uniswap V3 token pool address: {self.request_payload['uniswapV3PoolAddress']}", 2) + + self.log(f"Getting Panoptic token pool address...", 2) + self.log(f"POST /options/getPanopticPool [ connector: {self.connector}]", 0) + + self.request_payload.update({ + "univ3pool": self.request_payload['uniswapV3PoolAddress'] # redundant + }) + response = await GatewayHttpClient.get_instance().api_request( + method="post", + path_url="options/getPanopticPool", + params=self.request_payload, + fail_silently=False + ) + self.request_payload.update({ + "panopticPoolAddress": response["panopticPoolAddress"], + "panopticPool": response["panopticPoolAddress"], + }) + self.log(f"Panoptic token pool address: {self.request_payload['panopticPoolAddress']}", 1) + + self.log(f"Checking ticks...", 2) + self.log(f"POST /options/getTickSpacingAndInitializedTicks [ connector: {self.connector} ]", 0) + response = await GatewayHttpClient.get_instance().api_request( + method="post", + path_url="options/getTickSpacingAndInitializedTicks", + params=self.request_payload, + fail_silently=False + ) + self.tickSpacing=response['tickSpacing'] + self.tick_locations=response['ticks'] + self.log(f"Tick spacing: {self.tickSpacing}", 1) + self.log(f"Ticks: {self.tick_locations[0:10]}...{self.tick_locations[-10:]}", 2) + + self.wallet_address=self.address #redundant + + # TODO: Approve tokens and deposit + + self.initialized=True + + # continuously poll for transaction until confirmed + async def poll_transaction(self, chain, network, txHash): + pending: bool = True + while pending is True: + self.log(f"POST /network/poll [ txHash: {txHash} ]", 0) + pollData = await GatewayHttpClient.get_instance().get_transaction_status( + chain, + network, + txHash + ) + transaction_status = pollData.get("txStatus") + if transaction_status == 1: + self.log(f"Trade with transaction hash {txHash} has been executed successfully.", 1) + pending = False + elif transaction_status in [-1, 0, 2]: + self.log(f"Trade is pending confirmation, Transaction hash: {txHash}", 1) + await asyncio.sleep(2) + else: + self.log(f"Unknown txStatus: {transaction_status}", 1) + self.log(f"{pollData}", 2) + pending = False + + def log(self, message, triviality): + if (triviality <= self.verbosity): + self.logger().info(message) +``` + +Note that we indeed overrode the built-in `tick` counting of Hummingbot in favour of our own. There's likely a cleaner way to configure Hummingbot's ticks, but we ran into enough bugs with it that we chose to run with this strategy for now. + +--- +### Step 1: Define Strategy Configuration + +We first need to add some configuration values into our script. We'll conceive of our ideal positions to maintain with a few parameters: + +- `tick_to_maintain`: What ticks relative to the current tick do we want to be selling on? +- `max_utilisation`: What percentage of our position should be bought up at any given moment? +- `min_notional_value_to_sell_USD`: What minimum USD value of options should we be selling at this tick? +- `timescale`: What timescale should this position be? + +For example, we could save a target as the following dict: + +```python +targets = [ + {"tick_to_maintain": 0, "max_utilisation": 0.5, "min_notional_value_to_sell_USD": 1000, "timescale": "1M"} +] +``` + +--- +### Step 2: Querying about our open positions and the pool state around them +On each tick, we'll need to check if we're meeting our `targets`. We'll add this logic into `monitor_and_apply_logic`, which `on_tick` calls. + +First, we'll need to use the `queryPositions` endpoint to fetch our current positions: + +```python +response = await GatewayHttpClient.get_instance().api_request( + method="post", + path_url="options/queryPositions", + params=self.request_payload, + fail_silently=False +) +self.open_positions = response['openPositionIdList'] +self.log(f"Open position list: {self.open_positions}", 2) +``` + +Then, we'll loop through our targets, and see if any of the open positions are relevant to the target (in the sense that they match the `target`'s `strike` and `timescale`). Once we have the relevant positions, we can analyse our state. + +```python +for target in self.targets: + self.ontarget_positions = [] + for position in self.open_positions: + self.request_payload["tokenId"] = position + position_data = await GatewayHttpClient.get_instance().api_request( + method="post", + path_url="options/unwrapTokenId", + params=self.request_payload, + fail_silently=False + ) + # Check if the position meets the target tick and width + for leg in position_data['legInfo']: + strike = leg['strike'] + width = leg['width'] + target_strike = ph.get_valid_tick( + int(np.floor(self.tick_location - target['tick_to_maintain'])), + self.tickSpacing, + self.tickSpacing * ph.timescale_to_width(target['timescale'], self.tickSpacing) + ) + if strike == target_strike and width == ph.timescale_to_width(target['timescale'], self.tickSpacing): + # TODO: It's relevant! What next? +``` + +Next, inside that `if`, we'll check if the position is a sold-call or a sold-put - and if it is, we'll note that this accumulate the total value we're selling at this strike. We'll also accumulate a simple list of on-target positions so we can use that list for contract calls later. + +```python +# TODO: check if leg is a sold call or a sold put +# TODO: if it is: +# - get the USD notional value of what is being sold: +# - strike * positionSize * USD price of the asset +# - increment either volume_being_sold_at_target['call'] or volume_being_sold_at_target['put'] correspondingly +# Then, append this position onto ontarget_positions: +self.ontarget_positions.append(position) +``` + +Finally, after iterating through `self.open_positions`, we'll do a simple call to get the USD volume bought at our `target`'s strike & timescale, to calculate utilisation later: + +```python +# TODO: Next, query the subgraph for positions with any legs purchased against this tick/width +# - if found, increment usd_volume_bought_at_target appropriately based the notional value of each long leg of each position +``` + +--- +### Step 3: Analysing our positions compared to our targets + +With the relevant data in hand, our next step in handling this `target` is to compare the value being sold and bought with our target volumes and utilisations. There's two higher-level cases to think through: + +1. Is the volume of puts and calls we're selling equal? It will be if we're running this strategy out of the box, but maybe you're doing a little trading on the side or something. +2. Else, we have to handle the puts' and calls' comparisons to the target separately. + +Given that, we have a three branch if-else to implement: + +```python +# The volumes are equal: Therefore, just check if the calls being sold hit your minimum value, or +# if **either** your calls or your puts are exceeding the max utilisation +if (usd_volume_being_sold_at_target['call'] == usd_volume_being_sold_at_target['put']): + if ( + usd_volume_being_sold_at_target['call'] < target['min_notional_value_to_sell_USD'] || + ( + usd_volume_bought_at_target['call'] / usd_volume_being_sold_at_target['call'] > target['max_utilisation'] || + usd_volume_bought_at_target['put'] / usd_volume_being_sold_at_target['put'] > target['max_utilisation'] + ) + ): + usd_notional_value_to_sell = max( + target['min_notional_value_to_sell_USD'], + usd_volume_bought_at_target['put'] * (1 / target['max_utilisation']), + usd_volume_bought_at_target['call'] * (1 / target['max_utilisation']), + ) + # TODO: Do a burn-and-mint to burn ontarget_positions, then sell a straddle with a larger position size + # (Or, if ontarget_positions == [], just mint the straddle) + # How large? Well, enough to meet the minimum (and maybe a little more:) + # usd_notional_value_to_sell / (strike * USD price of the asset) + # TODO: I might need to modify burnAndMint to accept > 1 position to do this... + # TODO: in the future, some day, we should let people sell a strangles here instead of straddles in a + # config above + # TODO: then: + # - add new position to open_positions & ontarget_positions + # - remove burnt positions from open_positions & ontarget_positions +else if ( + usd_volume_being_sold_at_target['call'] < target['min_notional_value_to_sell_USD'] || + usd_volume_bought_at_target['call'] / usd_volume_being_sold_at_target['call'] > target['max_utilisation'] +): + usd_notional_value_to_sell = max( + target['min_notional_value_to_sell_USD'], + usd_volume_bought_at_target['call'] * (1 / target['max_utilisation']), + ) + # TODO: Filter ontarget_positions to just single-leg sold calls + # TODO: Then, burn-and-mint to burn the old calls and sell a new call at size: + # usd_notional_value_to_sell / (strike * USD price of the asset) + # TODO: then: + # - add new position to open_positions & ontarget_positions + # - remove burnt positions from open_positions & ontarget_positions +else if ( + usd_volume_being_sold_at_target['put'] < target['min_notional_value_to_sell_USD'] || + usd_volume_bought_at_target['put'] / usd_volume_being_sold_at_target['put'] > target['max_utilisation'] +): + # TODO: The same as the `call` case, but for puts +``` + +--- +### Step 4: Burning and reopening positions to accomplish our trading strategy + +Let's fill in the TODOs of those burn-and-remints, and then be sure to keep the `self.open_positions` up to date: + +```python +# TODO: Do a burn-and-mint to burn ontarget_positions, then sell a straddle with a larger position size +# (Or, if ontarget_positions == [], just mint the straddle) +# How large? Well, enough to meet the minimum (and maybe a little more:) +# usd_notional_value_to_sell / (strike * USD price of the asset) +# TODO: I might need to modify burnAndMint to accept > 1 position to do this... +# TODO: in the future, some day, we should let people sell a strangles here instead of straddles in a +# config above +# TODO: then: +# - add new position to open_positions & ontarget_positions +# - remove burnt positions from open_positions & ontarget_positions +``` + +Finally, we also might have other positions found in self.open_positions that are not helping us hit our targets. We want to cull these to ensure we stay under Panoptic's [25-position limit](/docs/contracts/parameters#max_open_legs-ethereum-mainnet). Here's how: + +```python +if !maintain_offtarget_positions: + offtarget_positions = [position for position in self.open_positions if position not in ontarget_positions] + # TODO: we need an endpoint to call burnOptions(tokenId[]); for now, iteratively calling burnOption(tokenId) + for offtarget_position in offtarget_positions: + updated_open_positions = [position for position in self.open_positions if position != offtarget_position] + + request_payload = { + "chain": chain, + "network": network, + "connector": connector, + "address": address, + "burnTokenId": offtarget_position, + "newPositionIdList": updated_open_positions, + "tickLimitLow": self.tickLimitLow, + "tickLimitHigh": self.tickLimitHigh + } + + burn_response = await GatewayHttpClient.get_instance().api_request( + method="post", + path_url="options/burn", + params=request_payload, + fail_silently=False + ) + + await self.poll_transaction(chain, network, burn_response['txHash']) + + # TODO: check tx success + # if it succeeded, update open_positions: + self.open_positions = updated_open_positions +``` + +### Step 5: + +And with that, you should be done! You can run this strategy by spinning up your Hummingbot Gateway: + +```bash +cd options_gateway +yarn start --passphrase=YourPassphrase +``` + +And then spin up Hummingbot: + +```bash +cd panoptic_hummingbot +conda activate hummingbot +./start +``` + +Then, from the Hummingbot Shell, connect to the Gateway and run the script: + +```bash +gateway connect panoptic +start --script panoptic_marketmaking_example.py +``` + +And you should see your script in action from the Logs pane! Happy market making! + +TODO: Insert screenshot of successful running