Lending protocols and short/long leverage data#

Load lending market data for a specific chain (Polygon in this example) and diplay status of the lending market.

In this specific example we download interest rates and prices for all assets on Aave v3 on Polygon.

As this notebook always uses latest data, it does not cache any downloaded datasets and may be slow to run depending on the speed of your Internet.

TODO: Some adjustments to this notebook are needed later, as the server-side data format will be switched from percents to raw floating points.

Set up client and API keys to download dataset#

[20]:
import datetime

import pandas as pd
from tradingstrategy.client import Client

# You will be interactively prompted to register an API key if you do not have one yet
client = Client.create_jupyter_client()
Started Trading Strategy in Jupyter notebook environment, configuration is stored in /Users/moo/.tradingstrategy

We set the notebook output to static image mode, so anyone can view this notetbook results directly on Github link.

Set back to interactive mode if you want to have draggable and zoomable charts in your notebook.

[21]:
from tradeexecutor.utils.notebook import OutputMode, setup_charting_and_output
setup_charting_and_output(OutputMode.static)

Download datasets#

We use a shortcut method load_trading_and_lending_data to grab everything we need.

  • This function can download current data

  • This function can download data for historical time periods

[22]:
from tradingstrategy.chain import ChainId
from tradeexecutor.strategy.universe_model import UniverseOptions
from tradeexecutor.strategy.execution_context import ExecutionMode, ExecutionContext
from tradeexecutor.strategy.trading_strategy_universe import load_trading_and_lending_data, TradingStrategyUniverse

# Download 30 days historical from today
lookback = datetime.timedelta(days=30)

# Even within Uniswap v3, there will be multiple trading pairs
# for each lending market asset, we need to specify
# which ones we are interested in.
# We pick USDC because it's most used stablecoin in DeFi
# environment.
#
# Even with USDC some assets are quoted
# with different Uniswap v3 fee tiers,
# like 30 BPS and 5 BPS.
# We deal with that later.
quote_token = "USDC"

# Load all trading and lending data on Polygon
# for all lending markets on a relevant time period
dataset = load_trading_and_lending_data(
    client,
    execution_context=ExecutionContext(mode=ExecutionMode.data_research),
    universe_options=UniverseOptions(history_period=lookback),
    # Ask for all Polygon data
    chain_id=ChainId.polygon,
    # We limit ourselves to price feeds on Uniswap v3 and Quickswap on Polygon,
    # as there are multiple small or dead DEXes on Polygon
    # which also have price feeds but not interesting liquidity
    exchange_slugs={"uniswap-v3", "quickswap"},

    reserve_asset_symbols={quote_token},

    # We want to display stablecoin-stablecoin
    # trading pairs
    volatile_only=False,
)

# Construct a trading universe that covers all
# chains, DEXes, trading pairs and lending pools we specified
strategy_universe = TradingStrategyUniverse.create_from_dataset(dataset)
data_universe = strategy_universe.data_universe

print(f"We have\n"
      f"- {data_universe.pairs.get_count()} trading pairs\n"
      f"- {data_universe.lending_reserves.get_count()} lending reserves\n"
      f"- {data_universe.candles.get_candle_count()} price candles ({data_universe.time_bucket.value})")
We have
- 19 trading pairs
- 20 lending reserves
- 373 price candles (1d)

Display available trading pair and lending asset data#

Display the status of the trading and lending markets.

We use get_pair_by_human_description() to find the lowest fee USDC trading pair for each lending reserve asset.

We determine if a trading pair is long/short leveraged tradeable by checking if it has enough trading volume on Polygon on any (indexed) DEX. The threshold is either - USD 100k monthly trading volume (no liquidity available) - USD 100k liquidity

[23]:
from IPython.display import HTML

from tradingstrategy.candle import CandleSampleUnavailable
from tradingstrategy.stablecoin import is_stablecoin_like
from tradingstrategy.pair import PairNotFoundError
from tradingstrategy.utils.jupyter import format_links_for_html_output

rows = []

now = datetime.datetime.utcnow()  # We always operate on naive UTC timesetamps
past = now - datetime.timedelta(days=29)

# We are still happy if we get 2 days old price
# (Some low liquidity pairs may see very few trades)
price_delay_tolerance = pd.Timedelta(days=2)

for reserve in data_universe.lending_reserves.iterate_reserves():

    stablecoin = is_stablecoin_like(reserve.asset_symbol)

    lending_link = reserve.get_link()

    supply_apr_now, lag = data_universe.lending_candles.supply_apr.get_single_rate(
        reserve,
        now,
        data_lag_tolerance=price_delay_tolerance,
    )

    borrow_apr_now, lag = data_universe.lending_candles.variable_borrow_apr.get_single_rate(
        reserve,
        now,
        data_lag_tolerance=price_delay_tolerance,
    )

    try:
        trading_pair = data_universe.pairs.get_pair_by_human_description((ChainId.polygon, None, reserve.asset_symbol, quote_token))
        exchange = data_universe.pairs.get_exchange_for_pair(trading_pair)
        trading_pair_label = f"{trading_pair.base_token_symbol}-{trading_pair.quote_token_symbol} at {trading_pair.fee} BPS fee tier on {exchange.name}"
        tradeable = trading_pair.is_tradeable()
        trading_pair_link = trading_pair.get_link()

        try:
            price_now, lag = data_universe.candles.get_price_with_tolerance(
                trading_pair,
                now,
                tolerance=price_delay_tolerance,
            )

            price_past, lag = data_universe.candles.get_price_with_tolerance(
                trading_pair,
                past,
                tolerance=price_delay_tolerance,
            )

            change = f"{(price_now - price_past) / price_past * 100:,.2f}"

        except CandleSampleUnavailable as e:
            price_now = "no trades"
            price_past = "no trades"
            change = ""

    except PairNotFoundError as e:
        # Some assets like AAVE do not have USDC quoted pair on Uniswap v3 on Polygon
        trading_pair = None
        trading_pair_label = "No markets available"
        tradeable = False
        price_now = ""
        price_past = ""
        trading_pair_link = ""
        change = ""

    rows.append({
        "Lending asset": reserve.asset_symbol,
        "Leveraged trading": "yes" if (not stablecoin and tradeable) else "no",
        "Stablecoin": "yes" if stablecoin else "no",
        "Trading volume": "yes" if tradeable else "no",
        "Best trading pair": trading_pair_label,
        "Price now (USD)": price_now,
        "Price 30d ago (USD)": price_past,
        "Price %": change,
        "Supply APR": f"{supply_apr_now:,.2f}",
        "Borrow APR": f"{borrow_apr_now:,.2f}",
        "Price data page": trading_pair_link,
        "Lending rate page": lending_link,
    })

df = pd.DataFrame(rows)
df = format_links_for_html_output(df, ("Price data page", "Lending rate page",))

print(f"Price and lending market information for {now} UTC")

with pd.option_context('display.max_rows', 100):
    display(HTML(df.to_html(escape=False)))

Price and lending market information for 2023-10-09 19:17:32.428722 UTC
Lending asset Leveraged trading Stablecoin Trading volume Best trading pair Price now (USD) Price 30d ago (USD) Price % Supply APR Borrow APR Price data page Lending rate page
0 AAVE no no no AAVE-USDC at 30 BPS fee tier on Quickswap 63.23903 54.18481 16.71 0.00 0.00 View View
1 agEUR no yes no No markets available 4.04 6.26 View View
2 BAL no no no BAL-USDC at 30 BPS fee tier on Uniswap v3 no trades no trades 4.77 15.54 View View
3 CRV no no no CRV-USDC at 30 BPS fee tier on Quickswap 0.447231 0.427878 4.52 0.33 4.88 View View
4 DAI no yes no No markets available 4.36 6.03 View View
5 DPI no no no DPI-USDC at 30 BPS fee tier on Quickswap no trades no trades 0.43 3.85 View View
6 EURS no yes no No markets available 2.66 4.60 View View
7 GHST no yes no No markets available 0.00 0.19 View View
8 jEUR no yes no No markets available 10.06 13.73 View View
9 LINK yes no yes LINK-USDC at 5 BPS fee tier on Uniswap v3 7.292186 6.012935 21.27 0.04 0.86 View View
10 MaticX no no no No markets available 0.00 0.33 View View
11 miMATIC yes no yes miMATIC-USDC at 30 BPS fee tier on Quickswap 0.872123 0.88318 -1.25 20.45 29.45 View View
12 stMATIC no no no stMATIC-USDC at 30 BPS fee tier on Quickswap no trades no trades 0.00 0.00 View View
13 SUSHI no no no SUSHI-USDC at 30 BPS fee tier on Uniswap v3 no trades no trades 1.91 6.10 View View
14 USDC no yes no No markets available 5.72 7.13 View View
15 USDT no yes no No markets available 3.92 5.39 View View
16 WBTC no no no WBTC-USDC at 30 BPS fee tier on Quickswap 27618.872905 25882.467764 6.71 0.01 0.31 View View
17 WETH yes no yes WETH-USDC at 5 BPS fee tier on Uniswap v3 1579.611806 1617.493841 -2.34 0.58 2.25 View View
18 WMATIC yes no yes WMATIC-USDC at 5 BPS fee tier on Uniswap v3 0.528472 0.523162 1.02 2.07 4.59 View View
19 wstETH no no no No markets available 0.01 0.43 View View

Historical lending rates#

Display historical supply and borrow APR for a single asset.

We plot the historical 30 days rates for ETH, using daily candles to show any variance.

[24]:
reserve = data_universe.lending_reserves.get_by_chain_and_symbol(ChainId.polygon, "WETH")

print("We are examining the lending reserve", reserve)

supply = data_universe.lending_candles.supply_apr.get_rates_by_reserve(reserve)
borrow = data_universe.lending_candles.variable_borrow_apr.get_rates_by_reserve(reserve)

# Display last 6 days
display(supply.iloc[-7:])
We are examining the lending reserve <LendingReserve for asset WETH in protocol aave_v3 on Polygon >
reserve_id open high low close timestamp
timestamp
2023-10-03 5 0.434257 0.594038 0.432984 0.584951 2023-10-03
2023-10-04 5 0.584952 0.596051 0.572082 0.572082 2023-10-04
2023-10-05 5 0.572082 0.588325 0.571232 0.578549 2023-10-05
2023-10-06 5 0.578546 0.589339 0.572514 0.573854 2023-10-06
2023-10-07 5 0.573851 0.582503 0.571838 0.574988 2023-10-07
2023-10-08 5 0.574953 0.583936 0.573015 0.574267 2023-10-08
2023-10-09 5 0.574236 0.592240 0.574218 0.580444 2023-10-09

First we draw the borrow APR as candles to show what the data format looks like.

Intra-month volatility in the lending rates is usually low, so candles do not look that interesting unless there has been lending rate spike events like high utilisation spikes due to flashloans.

[25]:
import plotly.graph_objects as go

borrow_candlestick = go.Candlestick(
    x=borrow.index,
    open=borrow['open'],
    high=borrow['high'],
    low=borrow['low'],
    close=borrow['close'],
    showlegend=False,
)

figure = go.Figure(
    borrow_candlestick,
    layout={
        "title": f"Borrow APR % for {reserve.asset_symbol} on {reserve.protocol_slug} on {reserve.chain_id.get_name()}",
        "xaxis_rangeslider_visible": False,
        "height": 600,
        "width": 1200,
    }
)
figure.show()
../../_images/programming_code-examples_lending-market-summary_13_0.svg

Then draw both supply APR and borrow APR % on the same chart. Use the daily close value for the intraday movement.

[26]:
import plotly.express as px

chart_df = pd.DataFrame(
    {
        "Borrow APR": borrow["close"],
        "Supply APR": supply["close"]
    },
    index=borrow.index,
)

figure = px.line(
    chart_df,
    title=f"Interest rates for {reserve.asset_symbol}",
    labels={
        "variable": "Rates",
        "timestamp": "Date",
    },
)
figure.show()

../../_images/programming_code-examples_lending-market-summary_15_0.svg

Wrap up#

For more examples see Trading Strategy developer documentation.

[ ]: