Excalibur

Excalibur is a desktop application for performant interface for decentralized finance build with iced. Iced is a cross-platform GUI library for Rust focused on simplicity and type-safety inspired by Elm. The application uses Arbiter and the rust ethereum ecosystem to communicate with both mock and live networks. We have decided to open source the project and enable outside feedback and contributions.

PHOTO OF APP GOES HERE

Application

Contract Interface

Excalibur delivers a custom contract interface. This lets us work with state changes in a much more human digestible way.

Agents

Agents are created as a client with an address. Each transaction can have a view that goes into more detail of whats going on in the transaction. Agents are the glue for the relationship between a simultion and strategy or offline and online.

When we run a sim, we are looking at the heuristics defined by the strategy, its what is important to us, since we plan on executing it. These, at the momeent, are more specific to the portfolio management app.

RPC Management

We should make it easy to manage rpcs. For example, anvil starts in the background of the app. But it creates an endpoint with a random port. We should be able to add an rpc, i.e. the background anvil, and use it for a provider connection.

Signer Management

In dev mode we load a local private key from an environment variable, which serves as a local signer. We should be able to add a signer, i.e. a hardware wallet, and make it an automatic choice when executing transactions (from address?).

Address Book

  • List of addresses, categorized and classified.
  • Selecting an address will copy it
  • Form to add addresses

Ledger

The ledger hardware communicates via the application protocol data unit (APDU). This unit is responsible for the communication between the ledger hardware and the host computer. The APDU is a binary format and is used to send commands to the ledger hardware and to receive responses from the ledger hardware. The protocol consists of a command response.

Command

An APDU command consists of the following fields:

FieldDescription
CLAClass of the instruction, indicates the type of the command interindustry or proprietary
INSInstruction code, this is defined by each ledger application, think of it as the application instruction set
P1 & P2Instruction parameters if the instruction has arguments
LcNumber of bytes present in the data field of the command.
DataData field of the command.
LeMaximum number of bytes expected in the data field of the response to the command.

Response

The response to an APDU command consists of the following fields:

FieldDescription
DataData field of the response.
SW1 & SW2Status word of the response.

A complete list of the status words can be found here.

Ledger Ethereum

The embedded Ethereum app on the ledger hardware is responsible for signing Ethereum transactions. This is the application we want to communicate with. The application has a set of instructions that it understands. The source code for the application is here. When a user updates their ethereum application they do it with the next version release of this repository.

Ledger Rust SDK Ecosystem

There are a few abstractions around this protocol different people have worked on. The most notable ones are:

  • Official Ledger SDK: I tried working with this one first. It was not bad and I still look at it for reference. There is also this other library Ledger Tauri. That is a good reference I still look at.
  • ether-rs ledger signer: I tried working with this one second because I was excited about having some generic abstraction over signers. Turns out this one is kind of broken. However, I did learn that it was built by James on top of this well-maintained library summa coins
  • summa-coins: This is the library I ended up building my solution on top of. It is also what will be used when rewriting the ledger-signer in alloy (or at least that's what James told me) so it felt like the best option. I was impressed with this library and how well it was built. It also has support for both bip32 and bip39.

Ledger Client

LedgerClient is the SDK in our repository built on top of the summa-coins library. It provides a minimal SDK around the Ledger Ethereum application. It currently supports a subset of all the ethereum ledger instructions but we can add more support later.

Utilization

When communicating with the ledger device we need to acquire a lock on the HIDTransport. This means that if there is another application talking to the ledger, this will not work. After obtaining the lock on the ledger we can interact with the ethereum application if it is open. If it is not open we will only be able to send instructions that return meta-data about the application. For example, we can check the version of the application.

use clients::ledger::LedgerClient;

let ledger = LedgerClient::new_connection(clients::ledger::types::DerivationType::LedgerLive(0)).await;

When creating a new ledger connection you must specify the account derivation path indicated by bitcoin improvement proposal 32 bip32. This allows for a hierarchical deterministic wallet. The derivation path is a string that looks like this m/44'/60'/0'/0/0. The first part of the path is the purpose. The second part is the coin type. The third part is the account. The fourth part is the change. The fifth part is the address index. The LedgerClient will use this path to derive the public key for the address index.

In most cases we will want to use the LedgerLive derivation type. This is the derivation path used by the ledger live application. The LedgerLive derivation type takes an index as an argument. This is the index of the account in the ledger live application. The LedgerLive derivation type will derive the derivation path for the account at the index.

When we have a ledger connection we can use it to sign transactions by giving it an ethers transaction request

use ethers::prelude::TransactionRequest;
use clients::ledger::LedgerClient;

let ledger = LedgerClient::new_connection(clients::ledger::types::DerivationType::LedgerLive(0)).await;

let tx = TransactionRequest::default();

// This currently correctly prompts the user to review this transaction
let sig = ledger.sign_tx(&tx).await.unwrap();
// once a user approves the transaction this will resolve and return the ethers signature type

Development

Miscellaneous development notes.

Testing CI/CD Locally

To test CI/CD locally, you can use Docker and Act. Here are the steps:

  1. Install Docker: You can download it from the official Docker website and follow the installation instructions.

  2. Install Act: Act is a tool that allows you to run your GitHub Actions locally. You can install it by running curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash. You can also install with brew install act if you are on a Mac.

  3. Run Act: Navigate to your project directory and run act. This will start the process of running your GitHub Actions locally.

Please note that Act uses the .github/workflows/ directory to find the workflow files, so ensure your workflow files are located there.

You can run a specific workflow by using the -j flag. For example, if you want to run the build job, you can run act -j build.

Contracts

Excalibur features a set of smart contracts designed by the Primitive team. These contracts have a single entry point, the DFMM.sol contract. The DFMM contract integrates with contracts that implement the IStrategy.sol interface. The strategy interface employs a unique validation mechanism that only accepts correct inputs. This way, the DFMM contract only makes valid state transitions.

Interfaces

The contracts work with two primary interfaces, DFMM and Strategy.

DFMM

The purpose of DFMM is to be the driver for the liquidity pools that can handle their states updating. It is the main touch point for users who want to be able to allocate or swap with pools. DFMM is agnostic for how the internal Strategy of the pool is happy to control pools that are created with a Strategy interface.

Strategy

The Strategy interface is the logic behind the operation of a specific DFMM pool. Specific implementations of the validateSwap and other methods must be implemented here. The openness of the interface allows developers to add their own features to the basic trading functions so that their strategies can be enveloped in the Strategy interface. For instance, we have some examples [here](LINK TO A PLACE WHERE WE HAVE SOME SPECIFIC STRATS).

Pool Updates

When the pool is told to update, we supply a new state of the pool that the contract only serves to verify is valid. This reduces the cost and complexity of the smart contract itself and offloads much of the compute to offchain resources. For example, if we want to compute a swap, we need to tell the pool a new value for $R_X$, $R_Y$, and $L$. If a wants to swap in some amount of $R_X$, then they want to get out as much $R_Y$ as possible. This computation often requires an optimization procedure that uses a bisection search which would be prohibitively expensive onchain yet proving the new state is valid essentially requires evaluation of one function.

Tokenization

The LP positions in pools are tokenized when the pool is created. We call these Liquidity Provider Tokens ($\texttt{LPT}$ s). When the pools are initialized a value for liquidity $L$ is determined, and this amount of $\texttt{LPT}$ s is granted to the initial LP. This LPT is an ERC-20 which can be used in other protocols for composability.

Liquidity Tracking

Since the pools over time can change parameters and accumulate fees, the $L$ of the pool changes and this is kept consistent with the $\texttt{LPT}$ s provided to the LPs. To do so, the pool tracks an effective exchange rate between the $\texttt{LPT}$ and the liquidity $L$ of the pool. For instance, at initialization, the exchange rate is 1 as one $\texttt{LPT}$ can be exchanged for deallocating 1 out of the total $L$. Suppose now that we have a swap come in and a fee changes $L \to L+\delta_L$, then the exchange rate would now be that 1 $\texttt{LPT}$ can exchange for $\frac{L+\delta_L}{L}$ out of the total $L$.

Dynamic Function Market Makers

Automated Market Makers (AMMs) have been extensively studied in DeFi and usually appear in the form of Constant Function Market Makers (CFMMs). CFMMs are a special case of a more general class of AMMs called Dynamic Function Market Makers (DFMMs). The main difference between CFMMs and DFMMs is that CFMMs maintain a constant invariant or bonding function while DFMMs can allow for parameters of the bonding function to change over time.

Examples

We will provide two examples of CFMMs and their DFMM counterparts. First is the Geometric Mean Market Maker (GMMM) and the second is the Log Normal Market Maker (LNMM). We will assume that pools have reserves $r_i$ for $i=1,\dots,n$ and that the trading function is $\varphi(\boldsymbol{r})$.

Geometric Mean Market Maker

The GMMM is a CFMM that maintains the following invariant: $$ \begin{equation} \varphi(\boldsymbol{r}) = \prod_{i=1}^n r_i^{w_i} - L \end{equation} $$ where $w_i$ are the weights, i.e., that $w_i$ is the weight of token $i$ and $L$ is the liquidity parameter. We also require that $w_i \in [0,1]$ and $\sum_{i=1}^n w_i = 1$.

We consider the state of the CFMM valid if and only if: $$ \begin{equation} \varphi(\boldsymbol{r}) = 0. \end{equation} $$ Dimensional analysis tells us that $L$ has dimensions of tokens.

The DFMM counterpart allows for the weights be arbitrary functions of time and even pool state. We will denote the varying weights by $w_i(t, \boldsymbol{q})$ where $\boldsymbol{q}$ is a choice of pool state, i.e., $$ \begin{equation} \boldsymbol{q} = \left(r_1, \dots, r_n, w_1, \dots, w_n, L\right). \end{equation} $$ For simplicity, consider a pool with two tokens $X$ and $Y$ with weights $w(t)$ and $1-w(t)$ respectively. Then the DFMM invariant is: $$ \begin{equation} \varphi(x,y,t) = r_x^{w(t)} r_y^{1-w(t)} - L. \end{equation} $$ For sake of concreteness, we can let $t \in [0,1]$ and take $w(t) = t$ so that the weights are linearly interpolated between $X$ and $Y$. Specifically, the pool will start out with $0$ weight on $X$ and $1$ weight on $Y$ and end with $1$ weight on $X$ and $0$ weight on $Y$. This can be thought of as a means of dollar cost averaging from $Y$ into $X$ over time $t$.

Log Normal Market Maker

The LNMM is a CFMM that maintains the following invariant: $$ \begin{equation} \varphi(\boldsymbol{r}) = \sum_{i=1}^n \Phi^{-1}\left( \frac{r_i}{p_i L} \right) + \sigma \sqrt{\tau} - L \end{equation} $$ where $\Phi^{-1}$ is the inverse of the cumulative distribution function of the standard normal distribution, $p_i$ is the relative strike price of token $i$, $\sigma$ is the volatility parameter, and $\tau$ is the time parameter.

Notation and Terminology

Trading Functions

Trading functions are a specific way of doing automated market making. Given a set of reserves and a single invariant, we can know how to compute a swap between any pair of assets. Trading functions are completely deterministic e.g. in their price discovery properties.

Quantities we can compute:

  • Price
  • Price impact
    • Volume to volatility at a given price
  • Liquidity

Notation

  • The tokens are given by: $$X \equiv \mathtt{token_x}$$ $$Y \equiv \mathtt{token_y} \equiv$$
  • The reserves are: $$ x \equiv \mathtt{reserve_x} $$ $$ y \equiv \mathtt{reserve_y} $$
  • The trading function is: $$ \varphi(x,y) \equiv \mathtt{trading_function} $$
  • The fee parameter is: $$ \gamma \equiv \mathtt{fee} $$
  • The amount changed will be written as $\delta$ with subscripts and will be given by: $$ \delta \equiv \mathtt{amount_deposit} $$ $$ \delta \equiv \mathtt{amount_withdrawn} $$ we can use a sign on $\delta$ to denote whether it is a deposit or withdrawal.
    • For liquidity deposits: $$ \delta_x \equiv \mathtt{amount_deposit_x} $$ $$ \delta_y \equiv \mathtt{amount_deposit_y} $$
    • For liquidity withdraws: $$ \delta_x \equiv \mathtt{amount_withdrawn_x} $$ $$ \delta_y \equiv \mathtt{amount_withdrawn_y} $$
  • The amount swapped is written as $\Delta$
  • The pool price is: $$ p \equiv \mathtt{pool_price} $$
  • The price is: $$ S \equiv \mathtt{price} $$ This can show up for arbitrage math for example

Computations

Pool Price

The pool price $p$ can be computed by: $$ p = \frac{\nabla_x \psi}{\nabla_y \psi} $$ This assumes that $Y$ is the numeraire token, so that this is a price of $X$ in terms of $Y$.

Liquidity Changes

When finding a valid change in liquidity, we must assert that the pool price $p$ remain invariant. That is: $$p(x,y) = p(x+\delta_x, y+\delta_y)$$

Swaps

When finding a valid (fee-less) swap, we must assert that the trading function remain invariant. That is $$\varphi(x,y) = \varphi(x+\Delta_x, y+\Delta_y)$$ This is, assuming there is no fee.

Including a Reinvested Fee

If we include a fee, then we must assert that the trading function remain invariant after the fee is taken and reinvested into the pool. The logic works this way, but when it comes to defining the equations, we can do so without having to take multiple steps. In essence:

  • The user inputs a $\Delta$ and $(1-\gamma)\Delta$ is taken as a liquidity change. This increases the liquidity first, and then the swap is done by taking the remaining $\gamma\Delta$ and using the equation of a swap without a fee.

So, we have, for a swap taking in $X$: $$ \varphi(x+\Delta_x, y+\Delta_y) = \varphi(x,y)\ \implies \Delta_y. $$ Then we must deposit $\delta_x = (1-\gamma) \Delta_x$ and from which it must be that: $$ p(x+\delta_x, x+\delta_y) = p(x,y)\ \implies \delta_y. $$ The user then receives back: $$ \widetilde{\Delta_x} = \Delta_y - \delta_y. $$ The new invariant value for the trading function is then: $$ \varphi(x+\delta_x, y+\delta_y) . $$

Price Impact

TODO

Interface

// Initialization
fn new(uint256 price, uint256 initial_x, uint256 initial_y, Parameters parameters) -> Pool;

// Read/getters
fn get_parameters() -> uint256;
fn get_price() -> uint256;
fn reserve_x() -> uint256;
fn reserve_y() -> uint256;

// Write/state changers
fn swap(bool input_token, uint256 amount_in) -> bool;
fn changeLiquidity(bool token, bool deposit, uint256 amount) -> bool;

// Structures
struct Parameters {
    fee: uint256,
    // ~ additional parameters
}

// High level events
event Swap(bool input_token, uint256 amount_in, uint256 amount_out);
event AddLiquidity(uint256 amount_x_in, uint256 amount_y_in);

// Logging events, used for debugging and analysis
event LogPrice(uint256 price);

Geometric Mean

This will be all the math involved with the Geometric Mean (G3M) trading function.

Conceptual Overview

G3M (for two assets) consists of a pool of reserves and their associated weights.

The G3M effectively gives the LP a portfolio that consists of a fixed ratio of the two assets based on the internal pricing mechanism. For instance, if we pick the weight of the $X$-token $0.80$ and $0.20$ for the $Y$-token, then the LP will have a portfolio that is 80% in $X$ and 20% $Y$ by price. This is a basic building block for a lot of portfolio designs.

Core

Mechanically, G3M of two variable parameters:

  • $w_x \equiv \mathtt{weight_x}$
  • $w_y \equiv \mathtt{weight_y}$
  • These parameters must satisfy $$ w_x, w_y \geq 0 \ w_x+w_y=1 $$

Next, we define the trading function to be: $$ \varphi(x,y) = x^{w_x} y^{w_y} = L $$ where $L$ is the invariant of the pool. We can put: $$ L \equiv \mathtt{liquidity} $$ Note that $L$ is in units of Token by virtue of the geometric mean.

Price

If we compute the derivatives and simplify the expression, we get that the pool price is: $$ \boxed{P = \frac{w_x}{w_y}\frac{y}{x}} $$ We can determine a price in terms of just $x$ or just $y$ if need be.

Initializing Pool

We need to initalize a pool from a given price $p$ and an amount of a token. We can also do it by specifying liquidity too.

Given x and price

Noting that $$ y= \frac{w_y}{w_x}p x $$ we can get $$ \begin{equation} \boxed{L_X(x,S) = x\left(\frac{w_y}{w_x}S\right)^{w_y}} \end{equation} $$ This is a linear function in $x$: $$ L_X(x+a\delta_x) = L_X(x) + aL_X(\delta_X) $$ We can get now the amount of $Y$ needed from $L$ and $x$ using the trading function and note: $$ \boxed{y(x,L;w_x) = \left(\frac{L}{x^{w_x}}\right)^{1/w_y}} $$

Given y and price

Noting that $$ x = \frac{w_x}{w_y}\frac{1}{p}y $$ we can get $$ \begin{equation} \boxed{L_Y(y,S) = y\left(\frac{w_x}{w_y}\frac{1}{S}\right)^{w_x}} \end{equation} $$ We can get now the amount of $X$ needed from $L$ and $y$ using the trading function and note: $$ \boxed{x(y,L;w_y) = \left(\frac{L}{y^{w_y}}\right)^{1/w_x}} $$

Swap

We require that the trading function remain invariant when a swap is applied, that is: $$ L(x,y) = (x+\Delta_x)^{w_x}(y+\Delta_y)^{w_y} $$ while also taking fees as a liquidity deposit (which will increase the liquidity $L$).

Trade in $\Delta_X$ for $\Delta_Y$

Suppose that we want to trade in $\Delta_X$ for $\Delta_Y$. Then we have that we are really inputting $\gamma\Delta_X$ while raising $L\mapsto L+\delta_L$. From Equation (1) we get that: $$ x = \frac{L}{\left(\frac{w_y}{w_x}S\right))^{w_y}} $$ and note that $L_X(x,p)$ is linear in $x$. Then we have that: $$ L_X(x+\delta_x) = L_X(x) + \delta_L \= L_X(x) + \delta_X(\frac{w_y}{w_x}p)^{w_y} $$ so $$ \boxed{\delta_{L_X} = \delta_X\left(\frac{w_y}{w_x}p\right)^{w_y}} $$ TODO: CAN REWRITE THIS WITHOUT PRICE

Hence we have for a swap with fees that (note $\Delta$ are what users input and receive): $$ L+\delta_L = (x+\gamma \Delta_X)^{w_x}(y+\Delta_y)^{w_y} $$ Then: $$ \boxed{\Delta_Y(\Delta_X) = \left(\frac{L+\delta_{L_X}}{(x+\Delta_X)^{w_x}}\right)^{1/w_y}-y} $$

Trade in $\Delta_Y$ for $\Delta_X$

We can get the $$ x = \frac{y}{p}\frac{w_x}{w_y} $$ We have the linear function: $$ \boxed{L_Y(y,S) = y\left(\frac{w_x}{w_y}\frac{1}{S}\right)^{w_x}} $$ so that: $$ \boxed{\delta_{L_Y} = \delta_Y\left(\frac{w_x}{w_y}\frac{1}{p}\right)^{w_x}} $$

Then $$ \boxed{\Delta_X(\Delta_Y) = \left(\frac{L+\delta_{L_Y}}{(y+\Delta_Y)^{w_y}}\right)^{1/w_x}-x} $$

Liquidity Provision

It must be that adding liquidity does not change the price of the pool. This makes it quite simple to add liquidity. If a user wants to add liquidity, they can just add the tokens such that the ratio of the reserves does not change. If a user wants to input $\Delta_x$ and $\Delta_y$ to the pool, then they must have: $$ p = \frac{w_x}{w_y} \frac{y}{x} = \frac{w_x}{w_y} \frac{y+\Delta_y}{x+\Delta_x} $$ which implies if they choose a given $\Delta_x$, then they must have: $$ \Delta_y = \frac{y}{x}(x+\Delta_x)-y $$ and similarly if they choose a given $\Delta_y$, then they must have: $$ \Delta_x = \frac{x}{y}(y+\Delta_y)-x $$

Arbitrage Math

We can solve for each variable in terms of the other and the invariant $k$: $$ x^{w_x}y^{w_y} = k $$

First, $x$: $$ \implies \boxed{x = \left(\frac{L}{y^{w_y}}\right)^{1/w_x} } $$

The work is analogous for $y$: $$ \implies \boxed{y = \left(\frac{L}{x^{w_x}}\right)^{1/w_y}} $$

Lowering Price

Suppose that we need the price to move $p\mapsto p'$ with $p'<p$. This means we tender $x$ in the swap so $x\mapsto x+\delta_x$. Then we want $p'$ and $x\mapsto x+\delta_x$: $$ p(x+\Delta_X,y+\Delta_Y) = \frac{w_x}{w_y}\frac{y+\Delta_Y}{x+\Delta_X} $$ Now we want to do this all for a given $p'$ and only with $X$. Note that $$ \Delta_Y(\Delta_X) = \left(\frac{L+\delta_L}{(x+\Delta_X)^{w_x}}\right)^{1/w_y}-y $$ Then using this: $$ x = \frac{L}{(\frac{w_y}{w_x}p)^{w_y}} $$ we can do $$ p' = \frac{w_x}{w_y}\frac{\left(\frac{L+\delta_L}{(x+\gamma \Delta_X)^{w_x}}\right)^{1/w_y}}{x+\gamma\Delta_X}\ (x+\gamma\Delta_X)^{1+w_x/w_y}=\frac{w_x}{p'w_y}(L+(1-\gamma)\Delta_X\left(\frac{w_y}{w_x}p\right)^{w_y})^{w_x}\ = \frac{1}{p'}\frac{w_x}{w_y}\left(\frac{w_y}{w_x}p\right)^{w_y}(x+(1-\gamma)\Delta_X)^{w_x}\ \implies (x+\Delta_x)^{1+w_x/w_y-w_x} = \frac{1}{p'}\frac{w_x}{w_y}\left(\frac{w_y}{w_x}p\right)^{w_y}\ \boxed{\Delta_x = \frac{1}{\gamma}\left(\left(L\frac{w_x}{w_y}\frac{1}{p'x}\right)^{\frac{1}{1+w_x/w_y-w_x}}-x\right)} $$

TRY AGAIN: $$ \Delta_x = \frac{1}{\gamma}\left(L \left( \frac{w_x}{pw_y}\right)^{w_y}+(1-\gamma) \Delta_x \right)\ \Delta_x + \frac{\gamma-1}{\gamma}\Delta_x = \frac{1}{\gamma}L \left( \frac{w_x}{pw_y}\right)^{w_y}\ \implies \boxed{\Delta_x = \frac{1}{\gamma}\left(L \left( \frac{w_x}{pw_y}\right)^{w_y}-x\right)} $$

Raising Price

Suppose that we need the price to move $p\mapsto p'$ with $p'>p$. This means we tender $x$ in the swap so $y\mapsto y+\delta_x$. Then we want $p'$ and $y\mapsto y+\delta_y$ with: $$ p' = \frac{w_x}{w_y}\frac{y+\delta_y}{x+\delta_x} $$ Now we can replace the $y+\delta_y$ with our equation above to get: $$ p'=\frac{w_x}{w_y}\frac{y+\delta_y}{\left( \frac{k}{(y+\delta_y)^{w_y}}\right)^{1/w_x}} $$ Then solving for $\delta_x$ yields $$ \implies \delta_y = \left(\frac{w_y}{w_x}p'k^{1/w_x}\right)^{\frac{1}{1+w_y/w_x}}-y $$

This can be simplified to: $$ \implies \boxed{ \delta_y = k\left(\frac{w_y}{w_x}p'\right)^{w_x}-y } $$

Value Function via $L$ and $S$

Given that we treat $Y$ as the numeraire, we know that the portfolio value of a pool when $X$ is at price $S$ is: $$ V(x,y,S) = x S + y $$ We can find the relationship to portfolio value from $V(L,S)$. This will be helpful when tokenizing pool LP positions.

Since we have $L_X(x, S)$ and $L_Y(y, S)$, we can get the following: $$ x = \frac{L}{(\frac{w_y}{w_x}S)^{w_y}}\ y = \frac{\left(\frac{w_x}{w_y}\frac{1}{S}\right)^{w_x}}{L} $$ Therefore: $$ V(L,S) = \frac{LS}{\left(\frac{w_y}{w_x}S\right)^{w_y}} + \frac{L}{\left(\frac{w_x}{w_y}\frac{1}{S}\right)^{w_x}}\ \boxed{V(L,S)=LS^{w_x}\left(\left( \frac{w_x}{w_y}\right)^{w_y}+\left( \frac{w_y}{w_x}\right)^{w_x}\right)} $$ Note that $V$ is linear in $L$ and so we can use this to tokenize.

Log Normal

This will be all the math involved with the Log Normal (LN?) trading function.

Conceptual Overview

The normal strategy provides the LP with a a log-normal shaped liquidity distribution centered around a price $K$ with a width given by $\sigma$. This strategy can be made time-dependent by an additional $\tau$ parameter that is the time til the pool will "expire". In this case, the LN trading function provides the LP with a payoff that is equivalent to a Black-Scholes covered call option with strike $K$, implied volatility $\sigma$, and time to expiration $\tau$. The parameters $K$ and $\sigma$ can also be made time dependent.

Core

LN has three variable parameters:

  • $K \equiv \mathtt{strike}$
  • $\sigma \equiv \mathtt{volatility}$
  • $\tau \equiv \mathtt{time_to_expiration}$
  • These parameters must satisfy: $$ K>0\ \sigma>0\ \tau>0 $$ Let's separate variables from parameters in function inputs with $;$ just to stay clear (though it may be verbose). The trading function for this strategy is given by $$ \begin{equation} \varphi(x,y,L;K,\sigma, \tau) = \Phi^{-1}\left(\frac{x}{L}\right)+\Phi^{-1}\left(\frac{y}{KL}\right)+\sigma \sqrt{\tau}. \end{equation} $$ In the equation above, $x$ and $y$ are reserves, and $L$ is the liquidity. We can put: $$ L \equiv \mathtt{liquidity} $$ Note that $L$ has units of Token (this is what we want). Given the domain of $\Phi^{-1}$ we can see that $x\in[0,L]$ and $y\in[0,KL]$. As the pool's liquidity increases, the maximal amount of each reserve increases and both are scaled by the same factor (this is what we want).

Useful Notation

We will use the following notation: $$ \begin{equation} d_1(S;K,\sigma,\tau) = \frac{\ln\frac{S}{K}+\frac{1}{2}\sigma^2 \tau }{\sigma \sqrt{\tau}} \end{equation} $$ $$ \begin{equation} d_2(S;K,\sigma,\tau) = \frac{\ln\frac{S}{K}-\frac{1}{2}\sigma^2 \tau }{\sigma \sqrt{\tau}} \end{equation} $$

Price

If we compute the derivatives and simplify the expression, we get that the pool price is given by either: $$ \begin{equation} \boxed{P_X(x, L; K, \sigma, \tau) = K \exp\left(\Phi^{-1} \left(1 - \frac{x}{L}\right) \sigma \sqrt{\tau} - \frac{1}{2} \sigma^2 \tau \right)} \end{equation} $$ $$ \begin{equation} \boxed{P_Y(y, L; K, \sigma, \tau) = K \exp\left(\Phi^{-1} \left(\frac{y}{KL}\right) \sigma \sqrt{\tau} + \frac{1}{2} \sigma^2 \tau \right)} \end{equation} $$ Do not that other DFMMs such as the geometric mean market maker have a price that can be determined from both reserves at once so we typically do not write P_X and P_Y.

Determining $L$ from Price and Reserves

There are a few distinct times where we need to determine the value of $L$, but they all come down to liquidity being deposited into the pool and not from swaps. We want to disentangle swaps and liquidity provision/donation. Note that for G3M, we don't have this same need as the $L$ is determined by the trading function explicitly.

$L$ from $x$

Without showing all the work, we can recall that $\frac{x}{L}$ is one of the option binaries: $$ \begin{equation} \frac{x}{L} = 1-\Phi((d_1(S;K,\sigma, \tau)) \end{equation} $$ Since we know $x$ and we know $S$, we can solve for $L$ to find: $$ \begin{equation} \boxed{L_X(x, S; K, \sigma, \tau) = \frac{x}{1-\Phi(d_1(S;K,\sigma, \tau))}} \end{equation} $$

$L$ from $y$

The work here is basically a mirrored image of the above. $$ \begin{equation} \frac{y}{KL} = \Phi(d_2(S;K,\sigma, \tau)) \end{equation} $$ From here we get $L$: $$ \begin{equation} \boxed{L_Y(y, S; K, \sigma, \tau) = \frac{y}{K\Phi(d_2(S;K,\sigma, \tau))}} \end{equation} $$

Pool Initialization

When the pool is initialized, we need to determine the value of $L$. The user will provide a price $S$ and an amount of $x$ or an amount of $y$ that they wish to tender. From there, we should be able to determine how much of both tokens must be allocated as well as the value of $L$.

Specifying $x$

Suppose that the user specifies the amount $x_0$ they wish to allocate and they also choose a price $S_0$. Further, we need to know how much $y_0$ to allocate, which we can also use the other binary in Equation $(8)$. At this point, we know $S$ and $L$ and so we can get: $$ \boxed{y_0 = y(x_0,S_0;K,\sigma, \tau) = K L_X(x_0, S_0; K, \sigma, \tau) \Phi(d_2(S;K,\sigma, \tau))} $$ Note that the above is not simplified and likely could be drastically simplified.

Specifying $y$

Suppose that the user specifies the amount $y$ they wish to allocate and they also choose a price $S$. Now we need to get $x$: $$ \boxed{x_0 = x(y_0, S_0) = L_Y(y_0, S_0; K, \sigma, \tau) \left(1-\Phi\left(d_1(S;K,\sigma, \tau)\right)\right)} $$

Adding/Removing Liquidity

When a user adds liquidity, they will specify an amount of $x$ or an amount of $y$, and the pool's price $S$ and liquidity $L$ will already be known. When adding liquidity, we assume that price will not change whatsoever and only the value of $L$ will change.

Specifying $x$

Given some amount of $\delta_x$ the user wants to add, we can just use the equation for $L(x,S)$ above to get: $$ L_X(x+\delta_x,S; K, \sigma, \tau) = \frac{x+\delta_x}{1-\Phi\left(d_1(S;K,\sigma, \tau)\right)} $$ In fact, $L$ is linear in the first variable, so: $$ L_X(x+\delta_x,S; K, \sigma, \tau) = L_X(x,S; K, \sigma, \tau)+\underbrace{L_X(\delta_x,S; K, \sigma, \tau)}{\delta_L} $$ can be used to make the calculation easier. In fact, if we just want to determine $\delta_L$, we can just use the above equation and subtract $L_X(x,S)$ from both sides and use our $S= P_X(x)$ equation to get: $$ \boxed{\delta{L,X} = \delta_X \frac{L}{x}} $$ where $x$ is the amount of $X$ reserves in the pool before the liquidity is added.

Specifying $y$

Given some amount of $\delta_y$ the user wants to add, we can just use the equation for $L(y,S)$ above to get: $$ L_Y(y+\delta_y,S; K, \sigma, \tau) = \frac{y+\delta_y}{K\Phi\left(d_1(S;K,\sigma, \tau)\right)} $$ Again, $L$ is linear in the first variable. We can use the same technique above to show that: $$ \begin{equation} \boxed{\delta_{L,Y} = \delta_Y \frac{L}{y}} \end{equation} $$

Getting the Inputs

Given that we have either $\delta_{L,X}$ or $\delta_{L,Y}$, we can use the above equations to get the amount of $X$ or $Y$ that must be added to the pool. Specifically, if the user wants to add $\delta_X$, we find $\delta_{L,X}$ then use Equation (10) like so: $$ \delta_{L,X} = \delta_Y \frac{L}{y}. $$ The other way follows mutatis mutandis.

Removing Liquidity

When a user removes liquidity, they will specify an amount of $x$ or an amount of $y$, and the pool's price $S$ and liquidity $L$ will already be known. When removing liquidity, we assume that price will not change whatsoever and only the value of $L$ will change. We can just use the same formulation as above and note that $\delta_x$ and $\delta_y$ may be positive or negative.

Swaps

When a user swaps, it must be that the trading function $\varphi$ remains invariant. Specifically, $$ \varphi(x+\Delta_X, y+\Delta_Y, L+\delta_L) = 0. $$ Note again I'm allowing for $\Delta_X$ and $\Delta_Y$ to be positive or negative. In absence of fees, the liquidity $L$ is invariant, that is $\delta_L = 0$. We will continue to use the lowercase $\delta$ for fee-based changes and $\Delta$ for swap inputs and outputs to distinguish these components. To compute a swap, it is a matter of finding the $\Delta_X(\Delta_Y)$ or $\Delta_Y(\Delta_X)$ that satisfies the above equation.

With Fees

Assume now that there is a fee parameter $\gamma$ such that the fee invested into the pool is $1-\gamma$. Assume further that the fee is always taken out of the input token for the swap. Think of the swap as a two step process:

  1. Adding liquidity. E.g., $\delta_Y \coloneqq (1-\gamma)\Delta_Y$. This is the amount of the input token that is added to the pool and it is what is used to calculate the change in liquidity $\delta_L$. From here, we can imagine that the swapper then takes temporary debt in adding $\delta_Y$ to the pool where the $\delta_Y$ can by found by using Equation $(10)$ and we specifically get: $$ \delta_Y = y \frac{\delta_L}{L} $$
  2. Computing a no-fee swap with the remaining amount of the input token. E.g., $\widetilde{\Delta_x} \coloneqq \gamma\Delta_x$. Note at this point, the reserves are then $x+\delta_x$ and $y+\delta_y$ and the liquidity $L+\delta_L$. So we must use these in the swap calculation. Then we can use all of the rules we defined here.

$\Delta_y$ given $\Delta_x$

Suppose that the user wants to swap $x$ for $y$ and the price is $S$. They specifically tender $\Delta_x$ and the fee parameter is $\gamma$. Now $\delta_x=(1-\gamma)\Delta_x$ and $\widetilde{\Delta_x}=\gamma\Delta_x$. From this we get $$ \delta_L=L_X(\delta_x, S)=\frac{\delta_x}{1-\Phi\left(\frac{\ln\frac{S}{K}+\frac{1}{2}\sigma^2}{\sigma}\right)} $$ Using the trading function, we solve for $\Delta_y$: $$ \Phi^{-1}\left(\frac{x+\Delta_x}{L+\delta_L}\right)+\Phi^{-1}\left(\frac{y+\Delta_y}{K(L+\delta_L)}\right)=-\sigma\

\boxed{\Delta_y(\Delta_x) = K(L+\delta_L)\cdot\Phi\left(-\sigma-\Phi^{-1}\left(\frac{x+\Delta_x}{L+\delta_L}\right)\right)-y} $$

$\Delta_x$ given $\Delta_y$

Suppose that the user wants to swap $y$ for $x$ and the price is $S$. They specifically tender $\Delta_y$ and the fee parameter is $\gamma$. Now $\delta_y=(1-\gamma)\Delta_y$ and $\widetilde{\Delta_y}=\gamma\Delta_y$. From this we get $$ \delta_L=L_Y(\delta_y, S)=\frac{\delta_y}{K\cdot\Phi\left(\frac{\ln\frac{S}{K}-\frac{1}{2}\sigma^2}{\sigma}\right)} $$ Using the trading function, we solve for $\Delta_x$: $$ \Phi^{-1}\left(\frac{x+\Delta_x}{L+\delta_L}\right)+\Phi^{-1}\left(\frac{y+\Delta_y}{K(L+\delta_L)}\right)=-\sigma\ \boxed{\Delta_x(\Delta_y) = (L+\delta_L)\cdot\Phi\left(-\sigma-\Phi^{-1}\left(\frac{y+\Delta_y}{K(L+\delta_L)}\right)\right)-x} $$

Arbitrage Math

Raising the price

When we need to raise the price, we need to tender in $Y$. If the current price is $S$ and we want to raise it to $S'$, then we need to tender in $Y$ such that we go from $y$ to $y'$ and: $$ y' = K\cdot (L+\delta_L) \cdot \Phi\left(\frac{\ln\frac{S'}{K}-\frac{1}{2}\sigma^2}{\sigma}\right) $$ and we know $\delta_L$ in terms of $\Delta_y$: $$ \frac{(1-\gamma) \Delta_y}{K\cdot\Phi\left(\frac{\ln\frac{S}{K}-\frac{1}{2}\sigma^2}{\sigma}\right)} $$ therefore the amount of $Y$ to tender is: $$ \Delta_y = y'-y = K\cdot (L+\delta_L) \cdot \Phi\left(\frac{\ln\frac{S'}{K}-\frac{1}{2}\sigma^2}{\sigma}\right)-y\ = K\cdot L \cdot \Phi\left(\frac{\ln\frac{S'}{K}-\frac{1}{2}\sigma^2}{\sigma} \right) + (1-\gamma)\Delta_y \cdot \frac{\Phi\left(\frac{\ln\frac{S'}{K}-\frac{1}{2}\sigma^2}{\sigma}\right)}{\Phi\left(\frac{\ln\frac{S}{K}-\frac{1}{2}\sigma^2}{\sigma}\right) } -y\ \implies \boxed{\Delta_y = \frac{KL\Phi\left(\frac{\ln\frac{S'}{K}-\frac{1}{2}\sigma^2}{\sigma}\right) - y}{1+(\gamma-1)\frac{\Phi\left(\frac{\ln\frac{S'}{K}-\frac{1}{2}\sigma^2}{\sigma}\right)}{\Phi\left(\frac{\ln\frac{S}{K}-\frac{1}{2}\sigma^2}{\sigma}\right) }}} $$

Lowering the price

When we need to lower the price, we need to tender in $X$. If the current price is $S$ and we want to lower it to $S'$, then we need to tender in $X$ such that we go from $x$ to $x'$ and: $$ \Delta x = (L + \delta_L)\cdot\left(1-\Phi\left(\frac{\ln\frac{S'}{K}+\frac{1}{2}\sigma^2}{\sigma}\right)\right) - x \ \implies \boxed{ \Delta_x = \frac{L\left(1-\Phi\left(\frac{\ln\frac{S'}{K}+\frac{1}{2}\sigma^2}{\sigma}\right)\right)-x}{1+(\gamma-1)\frac{1-\Phi\left(\frac{\ln\frac{S'}{K}+\frac{1}{2}\sigma^2}{\sigma}\right)}{1-\Phi\left(\frac{\ln\frac{S}{K}+\frac{1}{2}\sigma^2}{\sigma}\right)}}} $$

Value Function on $L(S)$

Relate to value on $V(L,S)$ and $V(x,y)$. Then we can use this to tokenize. We have $L_X(x, S)$ and $L_Y(y, S)$. We know that: $$ V(x(S),y(S)) = x S + y $$ Now we also have the following $$ x = LS\cdot\left(1-\Phi\left(\frac{\ln\frac{S}{K}+\frac{1}{2}\sigma^2}{\sigma}\right)\right)\ y = K\cdot L\cdot \Phi\left(\frac{\ln\frac{S}{K}-\frac{1}{2}\sigma^2}{\sigma}\right) $$ Therefore: $$ \boxed{V(L,S) = L\left( S\cdot\left(1-\Phi\left(\frac{\ln\frac{S}{K}+\frac{1}{2}\sigma^2}{\sigma}\right)\right) + K\cdot \Phi\left(\frac{\ln\frac{S}{K}-\frac{1}{2}\sigma^2}{\sigma}\right)\right)} $$ Note that $V$ is linear in $L$ and so we can use this to tokenize.

Time Dependence

Note that $L$ effectively changes as parameters of the trading function change. To see this, note that the trading function must always satisfy: $$ \Phi^{-1}\left(\frac{x}{L}\right)+\Phi^{-1}\left(\frac{y}{KL}\right) + \sigma \sqrt{\tau} = 0. $$ For new parameters, $K'$, $\sigma'$ and $\tau'$, we must find an $L'$ so that the trading function is satisfied: $$ \Phi^{-1}\left(\frac{x}{L'}\right)+\Phi^{-1}\left(\frac{y}{K'L'}\right) + \sigma' \sqrt{\tau'} = 0. $$ We can find this new $L'$ using a root finding algorithm.

Root Finding

We will use a bisection algorithm to determine the new $L'$. Suppose that $(K, \sigma, \tau, L)$ are the current parameters and we have $(K', \sigma', \tau')$ as the new parameters. Then we can compute: $$ f(L) = \Phi^{-1}\left(\frac{x}{L}\right)+\Phi^{-1}\left(\frac{y}{K'L}\right) + \sigma' \sqrt{\tau'}. $$

  • If $f(L)<0$, then:
    • Upper bound: $L$
    • Lower bound: $\max {x, y/K}$.
  • If $f(L)>0$, then:
    • Upper bound: $L \cdot 1.5$.
    • Lower bound: $L$

Simulation

General Strategy Interfacing

Arbitrageurs are agents that need to get specific pricing info from the arbitrage target. While the mechanics of the arbitrage might be different from each other, and require different variables, there is a universal set of actions and information they require.

Arbitrageur needs:

  • To get the current price of a pool that is the arbitrage target
    • Depends on smart contract call to pool
  • To get the reference market to fill the arbitrage with (target price)
    • Depends on price path / liquid exchange smart contract call
  • Get the amount out of tokens given an amount in
    • Depends on smart contract call to pool
  • Get the swap fee charged by the pool, since it affects the arbitrageur's incentive
    • Depends on smart contract call to pool
  • Compute the amount to swap and expect out, given the above information
    • Depends on arbitrageur's implementation, whether root finding or algebra
  • Execute the swap
    • Depends on smart contract call to arbitrageur's smart contract

Arbitrageur specifically needs to compute exactly the amount of tokens to swap to reach a target price that is within its desired arb bounds.

Sim flow:

  1. Create environment
  2. Create each agent as their own structs with implementations, which deploy their respective contracts.
  3. Initialize/add liquidity to the target pool
  4. Update the block
  5. Push the initial prices to the weight changer entity via weight_agent.init()
  6. Start the event logger
  7. Enter the loop over the price path
  8. Update the price on the price path
  9. Take a step on the arbitrageur loop
  10. Update the block
  11. Take a step on the weight changer agent

Agents

All agents that interact with the sim have a step that they take which runs their own logic. They also have a startup method which should be called outside of the main simulation loop to initialize their state.

These methods can be a trait that is implemented by the agents, then the agents that implement these traits can be put into a vec that is iterated over, and that's our main sim loop.

Strategies

Both the local agents, the arbitrageur and liquidity provider, need access to the Strategy methods to interact with the strategy, whether that's computing actions to take or taking the action. For example, the arbitrageur needs to get price info from the strategy's respective contracts, while the liquidity provider needs to initialize the pool and add tokens into it.

These agents are sharing the same interface, that can be a trait implemented over the contract instance.

Agents

Agents have behaviors that can influence the market. They can be used to simulate the behavior of different types of traders. They can also be used to take administrative control of AMMs. Our approach to agent design has been hierarchical in terms of simplicity. An agent is basic if it doesn't depend on any other agent's behavior.

Basic agents can be thought of as lower-level or more systemic, where their logic is consistent and non-dynamic. Some examples of basic agents are:

Block updater: This agent would allow you to progress the chain according to any questions you want.

Price Oracle: This agent is your price oracle allowing you to model oracle risk.

Token Admin: Token admins are responsible for deploying tokens and managing their approvals and mints.

Some examples of more complex agents are:

Liquidity Providers: These agents provide liquidity to the pool by allocating a portion of their portfolio. These agents are still non-dynamic in that they don't change their behavior based on other agents' behavior, but they depend on the token admin to mint tokens.

Some examples of dynamic agents are:

Arbitrageur: This agent arbitrages between pools. Traditionally, LPs are considered losing to the arbitrageurs in the short term. This agent's behavior depends on the token admin, price updater oracle, and liquidity provider. Since the price updater is an oracle, this agent also depends on the oracle.

Weight ChangerThis agent changes the weights of the pool. Depending on the heuristics of this pool, these agents vary.

Arbitrageur

Arbitrageur Agent

The main idea for designing the arbitrageur agent is to give them a set of exchanges to monitor and a set of actions to take. In the simplest case, we will have two exchanges. The agent will then monitor the exchanges and take actions when the price of a exchange deviates sufficiently from the price of the other exchange.

Necessary Attributes

The arbitrageur will need to be a structure that contains the following bits of data:

  • The attributes of the exchanges.
  • A client connection to each of the exchanges.

We will also give the arbitrageur a atomic_arbitrage contract so that they can use this to execute atomic arbitrages.

Necessary Methods

The arbitrageur will need to have the following methods:

  • A method to watch/update the prices of the exchanges.
  • A method to check for arbitrage opportunities.
  • A method to execute arbitrage opportunities.
  • Methods to compute the trade sizes to take.}

Other Notes

The arbitrageur could be ran asynchronously, but if there are not many agents in the simulation, it can be possible to iterate through the agents and have them take turns executing their actions.

Arbitrageur Agent

The main idea for designing the arbitrageur agent is to give them a set of exchanges to monitor and a set of actions to take. In the simplest case, we will have two exchanges. The agent will then monitor the exchanges and take actions when the price of a exchange deviates sufficiently from the price of the other exchange.

Necessary Attributes

The arbitrageur will need to be a structure that contains the following bits of data:

  • The attributes of the exchanges.
  • A client connection to each of the exchanges.

We will also give the arbitrageur a atomic_arbitrage contract so that they can use this to execute atomic arbitrages.

Necessary Methods

The arbitrageur will need to have the following methods:

  • A method to watch/update the prices of the exchanges.
  • A method to check for arbitrage opportunities.
  • A method to execute arbitrage opportunities.
  • Methods to compute the trade sizes to take.}

Other Notes

The arbitrageur could be ran asynchronously, but if there are not many agents in the simulation, it can be possible to iterate through the agents and have them take turns executing their actions.

Analysis

To validate our strategies, we need to be able to simulate them. This is done by creating a simulation environment that can be used to run the strategies and log the results. Once they are simulated, we can analyze the results to see how well the strategies performed. Simulation and analysis work together in tandem via the scientific method. We can use the results of the analysis to inform our next simulation, and so on. We can set out goals for performance, and then use the analysis to see if we are meeting those goals. These goals will also help us design analysis suites for individual strategies as well as give us ideas on what we consider to be general performance metrics.

General Performance Metrics

These are metrics that should be able to be tested across any strategy.

Raw contract-level performance metrics:

  • Gas costs for swaps, LP, maintenance.

Financial performance metrics:

  • Fee generation (with arbitrage flow and with retail flow)
  • Liquidity depth + gas costs -> fee generation
  • Sharpe ratio
  • Volatility compared to external market
  • Max drawdown compared to external market
  • High water mark compared to external market

Stability/Robustness performance metrics:

  • How does the financial performance change given incremental (from small to large) changes in parameter values. Do we ever see a critical point where a small change in choice of parameter leads to a large change in performance? If so, this is a sign of instability.
  • How does the performance handle shocks to the system?
  • Does the performance look smooth and "convex" around the optimal parameters? Or is it jagged and has multiple local optima?
  • What is the worst case scenario for the performance? How likely is it to occur?
  • What is the best case scenario for the performance? How likely is it to occur?

Security performance metrics:

  • Can the contract enter an unintended state that we consider anomalous?
  • Are there specific rational agents that can trigger a bad state?
  • Are there irrational agents that can trigger a bad state?
  • Are there boundary cases in the contract?
  • How does the contract fair after an immense amount of time and volume? (e.g., 1,000,000,000 swaps)

Dollar Cost Averaging

To analyze the DCA strategy we should consider the following approach.

  1. Define a weight changer agent that changes the weight of the DFMM curve over time as prescribed in Dollar Cost Averaging.
  2. Define a swapper agent to swap between $X$ and $Y$ at prescribed timeframes.

We can compare the values of the portfolio held by an LP into the DCA strategy versus the swapper agent.

Static Testing

Let's first test the DCA strategy with a single set of reasonable parameters. We can compare the DCA strategy in a GBM style market with a drift of $0.1$ and volatility of $0.5$ running over the course of $1.0$ years. Over the same time frame, we can let a swapper agent swap 12 times representing a monthly DCA purchase.

We can use the following structure for a toml:

[trajectory]
process = "gbm"
num_steps = 36500
seed = 1
num_paths = 50

[trajectory.initial_price]
fixed = 1.0
[trajectory.t_0]
fixed = 0.0
[trajectory.t_n]
fixed = 1.0

[gbm]
[gbm.drift]
fixed = 0.1
[gbm.volatility]
fixed = 0.25

[pool]
weight_x = 0.01
fee_basis_points = 30

[lp]
x_liquidity = 1.0

[weight_changer]
time_to_expiry = 1.0

[swapper]
num_swaps = 12

[block]
timestep_size = 15

Performance

Performance over the 50 paths will be used to see the initial efficacy of this system. We can plot the portfolio values of both the LP and the swapper and see how they track over time.

Volatility Targeting

The G3M volatility targeting strategy should have its own analysis harness dedicated to it. There are specific properties of this strategy that we want to test and analyze that extend beyond that scoped in the general performance metrics.

Notes

We should stick with a consistent choice of units for all of these simulations. Consider using a timeframe of a year.

Hypothesis Testing

The following are hypotheses that we can test with the volatility targeting strategy. They should be done independently, but we will need some overlap between them.

Adherence to Target Volatility

We should go through a slew of different approaches here, each more complicated and more realistic than the last. If we find bad results early on, then we can stop and not waste time on the more complicated approaches.

Constant Volatility

Aside from the initialization stage, the strategy should seek to maintain a constant volatility. To test this adherence, we should apply a range of GBM price paths with variable drifts and volatilities and collect data on how the strategy performs. We can generate statistics on the volatility of the strategy and compare it to the target volatility baseline.

Expectations:

  • A mean strategy volatility that is "close" to the target volatility
  • A standard deviation of the strategy volatility that is "close" to zero
  • Low to no dependence on the drift parameter

Testing:

  • Generate a GBM with sweeps:
    • $\mu \in {-1.0, -0.9, ..., 0.9, 1.0}$
    • $\sigma \in {0.05, 0.1, ..., 0.95, 1.0}$
    • Run for 50 random paths for each parameter.
    • Run each path for 100,000 steps over the course of 5 years.

Variable Volatility

The strategy should be able to handle variable volatility and track accordingly. To test this, we can use the same GBM price paths as above, but we can change the volatility at a given time step. We can then see how the strategy reacts to the change in volatility.

Expectations:

  • A mean strategy volatility that is "close" to the target volatility.
  • A standard deviation of the strategy volatility that is "close" to zero.
    • We should compute distance from the target volatility as a function of time. This result should be highly correlated to the time-varying volatility chosen for the process.

Testing:

  • Generate a GBM with sweeps:
    • $\mu \in {-1.0, -0.9, ..., 0.9, 1.0}$
    • $\sigma(t) \in {0.5\sin(t)+0.5, 0.5\sin(1.25t)+0.5, ..., 0.5\sin(3.0t)+0.5}$. By choosing sinusoidal volatility, we are allowing for a display of a smooth periodic change in volatility which is a very coarse model for seasonal volatility. We can choose here to amplify the frequency of the volatility change to see how the strategy reacts to a more rapid change in volatility.
    • Run for 50 random paths for each parameter.
    • Run each path for 100,000 steps over the course of 5 years.

Performance Metrics

Reweighting Speed

We have options for how we can choose to reweight the pool. We can choose to do so very quickly or very slowly -- each has its own tradeoffs.

Expectations:

  • Quicker reweighting leads to improved volatility tracking.
  • Quicker reweighting leads to more loss due to arbitrage.
  • Slower reweighting leads to worse volatility tracking.
  • Slower reweighting leads to less loss due to arbitrage.

Testing:

  • Generate a GBM with sweeps:
    • $\mu \in {-1.0, -0.9, ..., 0.9, 1.0}$
    • $\sigma(t) \in {0.5\sin(t)+0.5, 0.5\sin(1.25t)+0.5, ..., 0.5\sin(3.0t)+0.5}$.
    • Run for 50 random paths for each parameter.
    • Run each path for 100,000 steps over the course of 5 years.
  • Choose reweight time from $t \in {0.0005, 0.001, 0.0015, ..., 0.2}$