MakerDAO MCD Risk Tool
Analyzing different collateral type risk with models
Code will be availible once Readme has been reviewed
🗄 Table of Contents
- The Goal
- Setup
- Downloading The Prerequisites
- Cloning The Repo
- Loading the Models
- For Analysis
- Running the Models
- MCD Risk Model 1
- Authors
Goal of the Risk Tool:
Goal of the tool is to display risk and allow users to double click on visually evaluating risk.
💽 Setup:
If you would like to install and use this tool on your own machine make sure you have pipenv
installed. If you need to install follow these simple instructions.
Downloading The Prerequisites
The packages needed to run this tool: pipenv
, homebrew
, python3
, pandas
, and numpy
.
The packages pandas
and numpy
will need to be installed in order for these repositories to work. If you do not have them installed, run these commands in your terminal:
pipenv install pandas
pipenv install numpy
Cloning The Repo
Clone the MCD Risk Model Repo:
git clone https://github.com/madison1111/mcd_risk_model.git
Then cd into the directory:
cd mcd_risk_model
Loading the Models
To reproduce results run the following commands:
python3 mcd_risk_model_1.py
python3 mcd_risk_model_2.py
For Analysis
Start your Jupyter notebook:
jupyter notebook
If your browser doesn't automatically open, then go to:
http://localhost:8888
🎛 Running the Models
Notebooks can be found in the notebooks
folder
MCD Risk Model 1:
mcd_risk_model_1.py
Step 1:
Before doing anything else this script imports the various dependencies that allow for the script to be run in python as well as creates a class for the initial variables that we need
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import time
And create the class:
class mcd_model_1:
def __init__(self, start, end):
self.start = start
self.end = end
After, we look to call our data and will make two separate methods within our mcd_model_1 class, one for a single collateral type, and one for multiple collateral types
Data is encapsulated in Risk RESTful API
Historical Data
Collect trading history across all exchanges
Read in data
Data methodology is outlined in Risk API github repo
Historical exchange data
There is no ‘master exchange’ to pull from so we pull from several BTI verified exchanges, as price is determined by supply and demand and wash trading is a real problem among cryptocurrency exchanges
We pull data from 6 major exchanges: Coinbase
, Bitfinex
, Poloniex
, Bitstamp
, and Binance
and Kraken
to calculate an aggregate price index.
For example we can inspect the last 4 rows of the dataframe using the tail()
method
Close
- reversion benchmark
- Reversion metrics give us an indication of our temporary impact
Open
- momentum benchmark
Previous Close
- momentum benchmark
- Momentum metrics give us an indication of the direction of the price drift
Two Separate Methods
Define single collateral type:
def get_single_collateral(self, symbol):
#dates
start = self.start
end = self.end
prices = ETH['Close']
returns = prices.pct_change()
self.returns = returns
self.prices = prices
Define multiple collateral types:
def get_multiple_collateral(self, symbols, weights):
start = self.start
end = self.end
#Get exchange price data
df = ETH['Close']
#percent Change
returns = df.pct_change()
returns += 1
#define allocation of each asset
portfolio_value = returns * weights
portfolio_value['Portfolio Value'] = portfolio_value.sum(axis=1)
#Portfolio prices
prices = portfolio_value['Portfolio Value']
#portfolio returns
returns = portfolio_value['Portfolio Value'].pct_change()
returns = returns.replace([np.inf, -np.inf], np.nan)
self.returns = returns
self.prices = prices
Checking out momentum:
#Moving averages are a great way to determine the momentum of the cryptocurrency
ma_days = [10,20,50]
for ma in ma_days:
column_name = "MA %s days" %(str(ma))
ETH[column_name] = ETH['Adj Close'].rolling(window=ma,center=False).mean()
ETH[['Adj Close','MA 10 days','MA 20 days','MA 50 days']].plot(legend=True)
Moving average representation:
Moving average representation expresses any time series Yt as:
ETH output:
Once we have historical returns, we can gauge their relative dispersion with a measure called variance
Now we will calculate volatility and historical time-to-revert for collateralization ratio
Volatility: is calculated as the standard deviation of returns
Close to Close Volatility Method:
def close_to_close_volatility(close_ret, mean_ret):
return np.sqrt(np.sum((close_ret-mean_ret)**2)/390)
OHLC Volatility Estimation Method:
def crypto_volatility(open, high, low, close, close_tm_):
return np.log(open/close_tm1)**2 + 0.5*(np.log(high/low)**2) \
- (2*np.log(2)-1)*(np.log(close/open)**2)
CDP States:
bite
= debt-tranche that has been liquidatedwipe/shut
= debt-tranche that has been voluntarily closed by ownerunsafe
= CDP is below 150% collateralization ratiosafe
= CDP is not below 150% collateralization ratioopen
= CDP is safe and is still open
The time to revert for collateralization ratio looks at the CDP states and every state in the sequence has a timestamp (number of seconds the CDP stayed in that state)
- Calculate Transition state (infinitesimal generator matrix or intensity matrix)
- Look at the sequence distribution
- Answer: What fraction of all dai-time is spent in state ‘m’ before moving to state ‘k’?
- A matrix of the number of transition states are produced
- The next component of the code multiplies 2 matrices to get ‘draw_time’ in seconds
- The next component categories into safe and unsafe
- A final matrix is produced
sequesnces_with_next_state = utils.dataframe_to_sequences_with_end_state(df)
time_spent_matrix = utils.time_spent_before_state_change_distribution(sequence_with_next_state)
time_spent_matrix
Geometric Brownian Motion data: Volatility modeled in GBM function
Geometric Brownian Motion assumes that a constant drift is accompanied by random shocks and the period returns are normally distributed under GBM, the consequent multi-period price levels are lognormally distributed
Brownian motion models the random behavior of our collateral type over time
logarithmic_return = np.log(ETH.close)-np.log(close.shift(1))
mean_return = np.mean(returns)
volatility = returns.std()
GBM is composed of drift
and shock
Where:
drift
a long term trend in the positive or negative direction; derived from collaterals historical performance
shock
added or subtracted to the drift; function of the collaterals standard deviation
For each timestep, our model assumes the price will ‘drift’ up by the expected return
The drift will be ‘shocked’ (+/-) by a random shock and the random shock will be the standard deviation multiplied by a random number; this is a way of scaling the standard deviation
Pseudocode for GBM of ETH:
GBM without jump diffusion parameters:
Xo
initial collateral type price
mu
returns (drift coefficient)
sigma
volatility (diffusion coefficient)
W
brownian motion
T
time period
N
number of increments
def GBM(Xo, mu, sigma, W, T, N):
t = np.linspace(0.,1.,N+1)
X = []
X.append(Xo)
for i in xrange(1,int(N+1)):
drift = (mu - 0.5 * sigma**2) * t[i]
diffusion = sigma * W[i-1]
X_temp = Xo*np.exp(drift + diffusion)
X.append(X_temp)
return X, t
To capture drift and diffusion:
def daily_return(adj_close):
returns = []
for i in xrange(0, len(adj_close)-1):
today = adj_close[i+1]
yesterday = adj_close[i]
daily_return = (today - yesterday)/yesterday
returns.append(daily_return)
return returns
returns = daily_return(adj_close)
We use these simulations and visualize them:
while (simulation < number_simulations):
#list for each new simulation
prices = []
#reset the counter
days = 0
#input initial price
prices.append(last_price)
time_step = 1
while (days < predicted_days):
drift = (mean_return - volatility**2/2)*time_step
shock = volatility * np.random.normal() * time_step**0.5
price = prices[days] * np.exp(drift+shock)
prices.append(price)
#increment days counter
days = days + 1
results[str(simulation)] = pd.Series(prices).values
#increment simulation counter
simulation = simulation + 1
Behavioral Data
Analyzing CDP behavior and jump diffusion
To tune our model we are going to want to input: volatility
, probability_of_jump
, and mean_jump_size
.
In jump diffusion the idea is that price movements underlie sudden changes:
Jump diffusion will consider downtrends and captures the true picture of the collateral behavior:
sigma*np.randn()+mu
We can then calculate the jump size J by:
def jump(probability, mean_size, volatility):
jump_size = mean_size + volatility*np.random.randn()
return decision(probability)*jump_size
After, we are going to want to map the instance of the event under consideration of a probability using:
def decision_collateral(probability):
return int(np.random.random() < probability)
Taking the input data:
date
collateral_type
last_price
volatility
simulated_time_window
Time_window
results = pd.DataFrame()
simulation = 0
while (simulation < number_simulations):
prices = []
days = 0
prices.append(last_price)
time_step = 1
while (days < predicted_days):
drift = (mean_return - volatility**2/2)*time_step
shock = volatility * np.random.normal() * time_step**0.5
c_jump = jump(probability, mu, sigma)
price = prices[days] * np.exp(drift+shock)
price = price + c_jump
prices.append(price)
days = days + 1
if(prices[-1] > 0):
results[str(simulation)] = pd.Series(prices).values
simulation = simulation + 1
How we model GBM and Collateralization Ratio
We take this GBM with jump diffusion and the inertia reversion function to fluctuating collateralization ratio come in at ETH price path based on GBM with jump diffusion
ETH price path over time and second pass reads each day movement of ETH prices, ETH price moved x% and I am going to change collateralization ratio by y% and y is a function of x and the previous collateralization ratio and time
Evaluate liquidity by analyzing:
- % of day's volume
- intraday volume curve
- cumulative intraday volume curve, etc.
Traditionally, liquidity is highest as we approach the close and second highest at open.
Parameters for ‘expected amount of loss’:
crashes that are atypical;outright collateralization or significant enough sale through liquidation
The Value at Risk (VaR) for coverage is the maximum amount we could expect to lose with likelihood
- VaR is very similar to confidence intervals
- Conditional Value at Risk takes into account the shape of the returns distribution
Defining CVaR:
def cvar(invested, returns, weights, alpha=0.95, lookback=500):
var = value_at_risk(invested, returns, weights, alpha, lookback=lookback)
returns = returns.fillna(0.0)
portfolio_returns = returns.iloc[-lookback:].dot(weights)
# Get back to a return
var_pct_loss = var / invested
return invested * np.nanmean(portfolio_returns[portfolio_returns < var_pct_loss])
- CVaR captures the moments of the distribution; if the tails have more mass CVaR will capture this
- Following calculating CVaR check for convergence
Tail Risk:
- CVaR will take care of tail risk or fat tailedness
- Tail risk: autoregressive processes tend to have more extreme values than data taken from a normal distribution and this is because the value at each time point is influenced by recent values
- Fat tail distribution: if the series randomly jumps up, it is more likely to stay up than a non-autoregressive series, the extremes on the pdf will be fatter than the normal distribution
def calc_unadjusted_interval(X):
T = len(X)
mu = np.mean(X)
sigma = np.std(X)
lower = mu - 1.96 * (sigma/np.sqrt(T))
upper = mu + 1.96 * (sigma/np.sqrt(T))
return lower, upper
Statistical Rigor
Checking out visualizations of your data is not enough; we need a stronger degree of statistical rigor ie confidence intervals, pacf, etc.
Terms:
Volatility
is calculated as the standard deviation of returnsSlippage
is when the price 'slips' before the trade is fully executed, leading to the fill price being different from the price at the time of the order. Slippage is where our backtester calculates the realistic impact that your orders have on the execution price we receive.- The attributes that have the most influence on slippage are:
Volatility
Liquidity
Relative order size
Bid / Ask spread