36. APY Exploration

By Alex Stothart

2021-05-16

Here we explore the supplying and borrowing APY for each Compound market. We aim to see which markets are better or worse for suppliers, and which are better or worse for borrowers. We will observe how these trends vary over time, and will discuss what causes these trends.

In [1]:
## Import useful libraries and modules
import urllib.request as rq
import json
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import datetime as dt

We import the relevant market stat fields for the past 6 months and save it as a dataframe.

In [27]:
## Function to read data provided by SQL query "API" link
def loadData(url):
    try:
        dataset = rq.urlopen(url)
        dataset = dataset.read()
        dataset = json.loads(dataset)
    except Exception as e:
        print('Unable to get data from flipsidecrypto API. Check the URL below: \n{}'.format(url))
    return dataset

# Import Data from query link
url = "https://api.flipsidecrypto.com/api/v2/queries/6bba6233-536e-4b49-9a9a-8d4adde8fbff/data/latest"
dataset = loadData(url)
df = pd.DataFrame(dataset) # Convert imported dictionaries to dataframe
In [147]:
market_stats = df

We will now plot the supplying and borrowing APY values for each market over the past half year.

In [154]:
timestamps = []
for i in range(len(market_stats)):
    timestamps.append(dt.datetime.strptime(market_stats["BLOCK_HOUR"][i], "%Y-%m-%dT%H:%M:%SZ"))
market_stats["TIMESTAMP"] = timestamps
plt.figure(figsize=(10,10))
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'ETH']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'ETH']["SUPPLY_APY"], 'b', label='ETH')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'USDC']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'USDC']["SUPPLY_APY"], 'g', label='USDC')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'USDT']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'USDT']["SUPPLY_APY"], 'r', label='USDT')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'COMP']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'COMP']["SUPPLY_APY"], 'c', label='COMP')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'ZRX']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'ZRX']["SUPPLY_APY"], 'm', label='ZRX')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'BAT']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'BAT']["SUPPLY_APY"], 'y', label='BAT')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'DAI']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'DAI']["SUPPLY_APY"], 'k', label='DAI')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'UNI']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'UNI']["SUPPLY_APY"], 'orange', label='UNI')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'WBTC']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'WBTC']["SUPPLY_APY"], 'brown', label='WBTC')
plt.legend()
plt.title("Supply APY for each compound market over the past 6 months")
Out[154]:
Text(0.5, 1.0, 'Supply APY for each compound market over the past 6 months')
In [155]:
plt.figure(figsize=(10,10))
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'ETH']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'ETH']["BORROW_APY"], 'b', label='ETH')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'USDC']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'USDC']["BORROW_APY"], 'g', label='USDC')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'USDT']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'USDT']["BORROW_APY"], 'r', label='USDT')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'COMP']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'COMP']["BORROW_APY"], 'c', label='COMP')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'ZRX']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'ZRX']["BORROW_APY"], 'm', label='ZRX')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'BAT']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'BAT']["BORROW_APY"], 'y', label='BAT')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'DAI']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'DAI']["BORROW_APY"], 'k', label='DAI')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'UNI']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'UNI']["BORROW_APY"], 'orange', label='UNI')
plt.plot(market_stats[market_stats["UNDERLYING_SYMBOL"] == 'WBTC']["TIMESTAMP"], market_stats[market_stats["UNDERLYING_SYMBOL"] == 'WBTC']["BORROW_APY"], 'brown', label='WBTC')
plt.legend()
plt.title("Borrow APY for each compound market over the past 6 months")
Out[155]:
Text(0.5, 1.0, 'Borrow APY for each compound market over the past 6 months')

We see that the hourly values lead to a messy plot that is difficult to interpret, especially where the plots for markets overlap. Accordingly, we will take a daily average APY for both borrows and supplies for each market. This will result in fewer total datapoints, and a smoother figure that can be interpreted more clearly.

In [42]:
eth_stats = market_stats[market_stats["UNDERLYING_SYMBOL"] == 'ETH']
usdc_stats = market_stats[market_stats["UNDERLYING_SYMBOL"] == 'USDC']
usdt_stats = market_stats[market_stats["UNDERLYING_SYMBOL"] == 'USDT']
comp_stats = market_stats[market_stats["UNDERLYING_SYMBOL"] == 'COMP']
zrx_stats = market_stats[market_stats["UNDERLYING_SYMBOL"] == 'ZRX']
bat_stats = market_stats[market_stats["UNDERLYING_SYMBOL"] == 'BAT']
dai_stats = market_stats[market_stats["UNDERLYING_SYMBOL"] == 'DAI']
uni_stats = market_stats[market_stats["UNDERLYING_SYMBOL"] == 'UNI']
wbtc_stats = market_stats[market_stats["UNDERLYING_SYMBOL"] == 'WBTC']
In [132]:
dates = []
eth_daily_apy = np.zeros(190)
usdc_daily_apy = np.zeros(190)
usdt_daily_apy = np.zeros(190)
comp_daily_apy = np.zeros(190)
zrx_daily_apy = np.zeros(190)
bat_daily_apy = np.zeros(190)
dai_daily_apy = np.zeros(190)
uni_daily_apy = np.zeros(190)
wbtc_daily_apy = np.zeros(190)
for i in range(0,190,1):
    dates.append(dt.datetime.now().date() + dt.timedelta(days=-190+i))
    eth_daily_apy[i] = (eth_stats[np.logical_and(eth_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              eth_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_APY"].mean())
    usdc_daily_apy[i] = (usdc_stats[np.logical_and(usdc_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              usdc_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_APY"].mean())
    usdt_daily_apy[i] = (usdt_stats[np.logical_and(usdt_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              usdt_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_APY"].mean())
    comp_daily_apy[i] = (comp_stats[np.logical_and(comp_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              comp_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_APY"].mean())
    zrx_daily_apy[i] = (zrx_stats[np.logical_and(zrx_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              zrx_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_APY"].mean())
    bat_daily_apy[i] = (bat_stats[np.logical_and(bat_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              bat_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_APY"].mean())
    dai_daily_apy[i] = (dai_stats[np.logical_and(dai_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              dai_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_APY"].mean())
    uni_daily_apy[i] = (uni_stats[np.logical_and(uni_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              uni_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_APY"].mean())
    wbtc_daily_apy[i] = (wbtc_stats[np.logical_and(wbtc_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              wbtc_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_APY"].mean())
In [145]:
dates = []
eth_borrow_apy = np.zeros(190)
usdc_borrow_apy = np.zeros(190)
usdt_borrow_apy = np.zeros(190)
comp_borrow_apy = np.zeros(190)
zrx_borrow_apy = np.zeros(190)
bat_borrow_apy = np.zeros(190)
dai_borrow_apy = np.zeros(190)
uni_borrow_apy = np.zeros(190)
wbtc_borrow_apy = np.zeros(190)
for i in range(0,190,1):
    dates.append(dt.datetime.now().date() + dt.timedelta(days=-190+i))
    eth_borrow_apy[i] = (eth_stats[np.logical_and(eth_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              eth_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROW_APY"].mean())
    usdc_borrow_apy[i] = (usdc_stats[np.logical_and(usdc_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              usdc_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROW_APY"].mean())
    usdt_borrow_apy[i] = (usdt_stats[np.logical_and(usdt_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              usdt_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROW_APY"].mean())
    comp_borrow_apy[i] = (comp_stats[np.logical_and(comp_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              comp_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROW_APY"].mean())
    zrx_borrow_apy[i] = (zrx_stats[np.logical_and(zrx_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              zrx_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROW_APY"].mean())
    bat_borrow_apy[i] = (bat_stats[np.logical_and(bat_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              bat_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROW_APY"].mean())
    dai_borrow_apy[i] = (dai_stats[np.logical_and(dai_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              dai_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROW_APY"].mean())
    uni_borrow_apy[i] = (uni_stats[np.logical_and(uni_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              uni_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROW_APY"].mean())
    wbtc_borrow_apy[i] = (wbtc_stats[np.logical_and(wbtc_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              wbtc_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROW_APY"].mean())
In [158]:
plt.figure(figsize=(10,10))
plt.plot(dates, eth_daily_apy, 'b', label = 'ETH')
plt.plot(dates, usdc_daily_apy, 'g', label = 'USDC')
plt.plot(dates, usdt_daily_apy, 'r', label = 'USDT')
plt.plot(dates, comp_daily_apy, 'c', label = 'COMP')
plt.plot(dates, zrx_daily_apy, 'm', label = 'ZRX')
plt.plot(dates, bat_daily_apy, 'y', label = 'BAT')
plt.plot(dates, dai_daily_apy, 'k', label = 'DAI')
plt.plot(dates, uni_daily_apy, 'orange', label = 'UNI')
plt.plot(dates, wbtc_daily_apy, 'brown', label = 'WBTC')
plt.legend()
plt.title("Daily Average Supply APY")
Out[158]:
Text(0.5, 1.0, 'Daily Average Supply APY')

From looking at the plot, we see that the markets offering the highest returns for suppliers are USDT, USDC, and DAI. The lowest returns are offered by the WBTC, UNI, and ETH markets. The returns offered by the COMP, ZRX, and BAT markets generally fall in between, but have drifted downward recently, and settled closer to the WBTC, UNI, and ETH markets, offering low supply APY. We also see that there was a breif spike in the COMP supply APY in late 2020 to early 2021, but the value quickly returned to its typical level.

In [157]:
plt.figure(figsize=(10,10))
plt.plot(dates, eth_borrow_apy, 'b', label = 'ETH')
plt.plot(dates, usdc_borrow_apy, 'g', label = 'USDC')
plt.plot(dates, usdt_borrow_apy, 'r', label = 'USDT')
plt.plot(dates, comp_borrow_apy, 'c', label = 'COMP')
plt.plot(dates, zrx_borrow_apy, 'm', label = 'ZRX')
plt.plot(dates, bat_borrow_apy, 'y', label = 'BAT')
plt.plot(dates, dai_borrow_apy, 'k', label = 'DAI')
plt.plot(dates, uni_borrow_apy, 'orange', label = 'UNI')
plt.plot(dates, wbtc_borrow_apy, 'brown', label = 'WBTC')
plt.legend()
plt.title("Daily average borrow APY")
Out[157]:
Text(0.5, 1.0, 'Daily average borrow APY')

Looking at the borrow APY for each market, we see very similar trends to those observed in the supply markets with USDT, USDC, and DAI featuring the highest borrowing rates, and WBTC, UNI, and ETH with the lowest; however, there is generally less spread between the higher and lower APY markets than was seen for supplying.

We now wish to explore why these trends are observed. In the Compound protocol, APY values will be adjusted to incentivise borrowing or lending of a given asset to meet the needs determined by the supply and borrowing demand in each market. The rates are most-likely calculated automatically via the protocol smart contract based on the current quantity of assets supplied, and the expected borrowing demand in each market. The expected borrowing demand is likely estimated based current amount borrowed, and recent borrowing trends.

If a market has a large supply that far exceeds the borrowing demand, there is little need for further supply, and the supply APY will be set to a small value accordingly. The borrow rate will also be set to a low value to incentivize interaction with the protocol through this asset, as the pool can easily afford to accomodate further borrowing. Conversely, if large quantities are being borrowed, and there is relatively little supply of the asset, supply APY will be set to a high value to incentivize further supplying of the asset, and the borrow rate will also be set to a high value to disincentivize borrowing until additional supply is obtained.

To comfirm our suspicion of the mechanism driving the supply and borrow APY, we will look at the ratio of borrowed value to supplied value for each asset market.

In [134]:
dates = []
eth_supply = np.zeros(190)
usdc_supply = np.zeros(190)
usdt_supply = np.zeros(190)
comp_supply = np.zeros(190)
zrx_supply = np.zeros(190)
bat_supply = np.zeros(190)
dai_supply = np.zeros(190)
uni_supply = np.zeros(190)
wbtc_supply = np.zeros(190)
for i in range(0,190,1):
    dates.append(dt.datetime.now().date() + dt.timedelta(days=-190+i))
    eth_supply[i] = (eth_stats[np.logical_and(eth_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              eth_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_USD"].mean())
    usdc_supply[i] = (usdc_stats[np.logical_and(usdc_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              usdc_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_USD"].mean())
    usdt_supply[i] = (usdt_stats[np.logical_and(usdt_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              usdt_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_USD"].mean())
    comp_supply[i] = (comp_stats[np.logical_and(comp_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              comp_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_USD"].mean())
    zrx_supply[i] = (zrx_stats[np.logical_and(zrx_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              zrx_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_USD"].mean())
    bat_supply[i] = (bat_stats[np.logical_and(bat_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              bat_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_USD"].mean())
    dai_supply[i] = (dai_stats[np.logical_and(dai_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              dai_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_USD"].mean())
    uni_supply[i] = (uni_stats[np.logical_and(uni_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              uni_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_USD"].mean())
    wbtc_supply[i] = (wbtc_stats[np.logical_and(wbtc_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              wbtc_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["SUPPLY_USD"].mean())
In [135]:
dates = []
eth_borrows = np.zeros(190)
usdc_borrows = np.zeros(190)
usdt_borrows = np.zeros(190)
comp_borrows = np.zeros(190)
zrx_borrows = np.zeros(190)
bat_borrows = np.zeros(190)
dai_borrows = np.zeros(190)
uni_borrows = np.zeros(190)
wbtc_borrows = np.zeros(190)
for i in range(0,190,1):
    dates.append(dt.datetime.now().date() + dt.timedelta(days=-190+i))
    eth_borrows[i] = (eth_stats[np.logical_and(eth_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              eth_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROWS_USD"].mean())
    usdc_borrows[i] = (usdc_stats[np.logical_and(usdc_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              usdc_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROWS_USD"].mean())
    usdt_borrows[i] = (usdt_stats[np.logical_and(usdt_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              usdt_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROWS_USD"].mean())
    comp_borrows[i] = (comp_stats[np.logical_and(comp_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              comp_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROWS_USD"].mean())
    zrx_borrows[i] = (zrx_stats[np.logical_and(zrx_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              zrx_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROWS_USD"].mean())
    bat_borrows[i] = (bat_stats[np.logical_and(bat_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              bat_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROWS_USD"].mean())
    dai_borrows[i] = (dai_stats[np.logical_and(dai_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              dai_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROWS_USD"].mean())
    uni_borrows[i] = (uni_stats[np.logical_and(uni_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              uni_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROWS_USD"].mean())
    wbtc_borrows[i] = (wbtc_stats[np.logical_and(wbtc_stats["BLOCK_HOUR"] >= (dt.datetime.now().date() + dt.timedelta(days=(-190+i))).strftime("%Y-%m-%dT%H:%M:%SZ"), 
                                                              wbtc_stats["BLOCK_HOUR"] <= (dt.datetime.now().date() + dt.timedelta(days=(-189+i))).strftime("%Y-%m-%dT%H:%M:%SZ"))]["BORROWS_USD"].mean())
In [136]:
eth_ratio = eth_borrows/eth_supply
usdc_ratio = usdc_borrows/usdc_supply
usdt_ratio = usdt_borrows/usdt_supply
comp_ratio = comp_borrows/comp_supply
zrx_ratio = zrx_borrows/zrx_supply
bat_ratio = bat_borrows/bat_supply
dai_ratio = dai_borrows/dai_supply
uni_ratio = uni_borrows/uni_supply
wbtc_ratio = wbtc_borrows/wbtc_supply

We now plot the calculated ratio for each market over time.

In [160]:
plt.figure(figsize=(10,10))
plt.plot(dates, (eth_ratio), 'b', label = 'ETH')
plt.plot(dates, (usdc_ratio), 'g', label = 'USDC')
plt.plot(dates, (usdt_ratio), 'r', label = 'USDT')
plt.plot(dates, (comp_ratio), 'c', label = 'COMP')
plt.plot(dates, (zrx_ratio), 'm', label = 'ZRX')
plt.plot(dates, (bat_ratio), 'y', label = 'BAT')
plt.plot(dates, (dai_ratio), 'k', label = 'DAI')
plt.plot(dates, (uni_ratio), 'orange', label = 'UNI')
plt.plot(dates, (wbtc_ratio), 'brown', label = 'WBTC')
plt.legend()
plt.title("Ratio of borrowed asset value to supplied value for each market")
Out[160]:
Text(0.5, 1.0, 'Ratio of borrowed asset value to supplied value for each market')

The above plot supports our suspicion. As we see, the markets with the highest borrowing and supplying rates (USDT, USDC, DAI), also have the highest ratio of borrowed value to supplied value. This means that the markets are having a harder time keeping up with the borrowing demand and maintaining a safe buffer, so further supply must be incentivized, and further borrowing disincentivized. Additionally, the markets with the lowest supply and borrow APY (ETH, UNI, WTBC) also have the lowest ratio of borrowed value to supplied value. ZRX, BAT, and COMP once again fall somewhere in between.

We also notice that the spike in APY for the COMP market matches to a corresponding spike in the borrowed value to supplied value ratio, likely resulting from a large borrow or redemption occuring. We see that the magnitude of the spike in APY compared to other markets is much higher than the spike in the ratio value, which suggests that projected borrow demand likely involves some form of forecasting based on recent activity, and does not simply come from the total borrowed value. The spike in activity likely caused a larger jump in projected borrowing demand than it did in total borrowed value, and this lead to a disproportionately large spike in APY rates.

It is also worth comparing the above results to what we would expect in traditional, centralized lending. In traditional banking, borrowing interest rates primarily result based on the perceived risk assumed by the lender. If this were the case here, the more volatile assets would likely have the highest interest rates, as rapidly changing asset values would lead to a higher risk of being undercollateralized, however, here we see the opposite -- the stablecoins (USDC, USDT, and DAI) feature the highest borrowing interest rates, despite being the least volatile, and least likely to dip in value and result in undercollateralization. The explanation for these results that run against traditional financial intuition can likely be attributed to two causes. The first is the collateralization and liquidation system that exists in Compound protocol. Because of this system, loans do not default the way they do in traditional finance, and the risk is inherently minimized. The second reason is that cryptocurrencies in general have been consistently increasing in value, and most crypto investors want to take advantage of the rapidly increasing value of their assets. Stablecoins that attempt to remain pegged at 1 USD will not experience this same increase in value over time. This means that Compound users would rather supply other crypto assets, so that they can take advantage of both the APY, and the increasing value of the assets themselves. This means that the protocol has a harder time maintaining a high supply of stablecoins, and as a result must incentivize the supplying of the assets, and disincentivize their borrowing.

In [ ]: