Skip to content

Commit

Permalink
Estimate interest (#127)
Browse files Browse the repository at this point in the history
- Move `estimate_accrued_interes` to `LendingCandleUniverse`
- Better exception handling if no data
- Cover with tests
  • Loading branch information
miohtama authored Nov 2, 2023
1 parent 11c583e commit 19b84b1
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 3 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 0.20.16

- Add `LendingMetricUniverse.estimate_accrued_interest` so we can estimate the position cost and interest profit
in backtesting

# 0.20.15

- Update candle mappings so volume doesn't get set to None
Expand Down
67 changes: 66 additions & 1 deletion tests/test_lending.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""Client lending dataset download and integrity tests"""

import logging
from _decimal import Decimal
from pathlib import Path

import pandas as pd
Expand All @@ -12,7 +13,8 @@
filter_for_base_tokens
from tradingstrategy.timebucket import TimeBucket
from tradingstrategy.client import Client
from tradingstrategy.lending import LendingCandleType, LendingReserveUniverse, LendingProtocolType, UnknownLendingReserve, LendingReserve, LendingCandleUniverse
from tradingstrategy.lending import LendingCandleType, LendingReserveUniverse, LendingProtocolType, UnknownLendingReserve, LendingReserve, \
LendingCandleUniverse, NoLendingData
from tradingstrategy.utils.time import ZERO_TIMEDELTA

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -306,3 +308,66 @@ def test_get_single_rate(persistent_test_client: Client):
assert lag == ZERO_TIMEDELTA


def test_get_estimate_interest(persistent_test_client: Client):
"""Estimate how much profit we make with USDC credit.
Estimate borrow cost and supply profit for a year.
"""

client = persistent_test_client
lending_reserves = client.fetch_lending_reserve_universe()

usdc_desc = (ChainId.polygon, LendingProtocolType.aave_v3, "USDC")

lending_reserves = lending_reserves.limit([usdc_desc])

lending_candle_type_map = client.fetch_lending_candles_for_universe(
lending_reserves,
TimeBucket.d1,
start_time=pd.Timestamp("2022-09-01"),
end_time=pd.Timestamp("2023-09-01"),
)
lending_candles = LendingCandleUniverse(lending_candle_type_map, lending_reserves)

# Estimate borrow cost
borrow_interest_multiplier = lending_candles.variable_borrow_apr.estimate_accrued_interest(
usdc_desc,
start=pd.Timestamp("2022-09-01"),
end=pd.Timestamp("2023-09-01"),
)
assert borrow_interest_multiplier == pytest.approx(Decimal('1.028597760665127969909038441'))

# Estimate borrow cost
supply_interest_multiplier = lending_candles.supply_apr.estimate_accrued_interest(
usdc_desc,
start=pd.Timestamp("2022-09-01"),
end=pd.Timestamp("2023-09-01"),
)
assert supply_interest_multiplier == pytest.approx(Decimal('1.017786465640168974688961612'))


def test_estimate_interest_bad_timeframe(persistent_test_client: Client):
"""Try estimate interest when we have no data."""

client = persistent_test_client
lending_reserves = client.fetch_lending_reserve_universe()

usdc_desc = (ChainId.polygon, LendingProtocolType.aave_v3, "USDC")

lending_reserves = lending_reserves.limit([usdc_desc])

lending_candle_type_map = client.fetch_lending_candles_for_universe(
lending_reserves,
TimeBucket.d1,
start_time=pd.Timestamp("2022-09-01"),
end_time=pd.Timestamp("2023-09-01"),
)
lending_candles = LendingCandleUniverse(lending_candle_type_map, lending_reserves)

# Ask for non-existing period
with pytest.raises(NoLendingData):
lending_candles.variable_borrow_apr.estimate_accrued_interest(
usdc_desc,
start=pd.Timestamp("2020-01-01"),
end=pd.Timestamp("2020-01-02"),
)
104 changes: 102 additions & 2 deletions tradingstrategy/lending.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
See :py:class:`LendingReserveUniverse` on how to load the data.
"""
import warnings
from _decimal import Decimal
from enum import Enum
import datetime

Expand All @@ -21,6 +22,8 @@
from tradingstrategy.types import UNIXTimestamp, PrimaryKey, TokenSymbol, Slug, NonChecksummedAddress, URL
from tradingstrategy.utils.groupeduniverse import PairGroupedUniverse

from eth_defi.aave_v3.rates import SECONDS_PER_YEAR


class LendingProtocolType(str, Enum):
"""Supported lending protocols."""
Expand All @@ -38,6 +41,12 @@ class UnknownLendingReserve(Exception):
"""Does not know about this lending reserve."""


class NoLendingData(Exception):
"""Lending data missing for asked period.
Reserve was likely not active yet.
"""


@dataclass_json
@dataclass
Expand Down Expand Up @@ -142,7 +151,7 @@ def __hash__(self):
return hash((self.chain_id, self.protocol_slug, self.asset_address))

def __repr__(self):
return f"<LendingReserve #{self.reserve_id} for asset {self.asset_symbol} in protocol {self.protocol_slug.name} on {self.chain_id.get_name()} >"
return f"<LendingReserve #{self.reserve_id} for asset {self.asset_symbol} ({self.asset_address}) in protocol {self.protocol_slug.name} on {self.chain_id.get_name()} >"

def get_asset(self) -> Token:
"""Return description for the underlying asset."""
Expand Down Expand Up @@ -613,6 +622,95 @@ def get_single_rate(
link=link,
)

def estimate_accrued_interest(
self,
reserve: LendingReserveDescription | LendingReserve,
start: datetime.datetime | pd.Timestamp,
end: datetime.datetime | pd.Timestamp,
) -> Decimal:
"""Estimate how much credit or debt interest we would gain on Aave at a given period.
Example:
.. code-block:
lending_reserves = client.fetch_lending_reserve_universe()
usdc_desc = (ChainId.polygon, LendingProtocolType.aave_v3, "USDC")
lending_reserves = lending_reserves.limit([usdc_desc])
lending_candle_type_map = client.fetch_lending_candles_for_universe(
lending_reserves,
TimeBucket.d1,
start_time=pd.Timestamp("2022-09-01"),
end_time=pd.Timestamp("2023-09-01"),
)
lending_candles = LendingCandleUniverse(lending_candle_type_map, lending_reserves)
# Estimate borrow cost
borrow_interest_multiplier = lending_candles.variable_borrow_apr.estimate_accrued_interest(
usdc_desc,
start=pd.Timestamp("2022-09-01"),
end=pd.Timestamp("2023-09-01"),
)
assert borrow_interest_multiplier == pytest.approx(Decimal('1.028597760665127969909038441'))
# Estimate borrow cost
supply_interest_multiplier = lending_candles.supply_apr.estimate_accrued_interest(
usdc_desc,
start=pd.Timestamp("2022-09-01"),
end=pd.Timestamp("2023-09-01"),
)
assert supply_interest_multiplier == pytest.approx(Decimal('1.017786465640168974688961612'))
:param reserve:
Asset we are interested in.
:param start:
Start of the period
:param end:
End of the period
:return:
Interest multiplier.
Multiply the starting balance with this number to get the interest applied balance at ``end``.
1.0 = no interest.
"""

if isinstance(start, datetime.datetime):
start = pd.Timestamp(start)

if isinstance(end, datetime.datetime):
end = pd.Timestamp(end)

assert isinstance(start, pd.Timestamp), f"Not a timestamp: {start}"
assert isinstance(end, pd.Timestamp), f"Not a timestamp: {end}"

assert start <= end

df = self.get_rates_by_reserve(reserve)

# TODO: Can we use index here to speed up?
candles = df[(df["timestamp"] >= start) & (df["timestamp"] <= end)]

if len(candles) == 0:
raise NoLendingData(f"No lending data for {reserve}, {start} - {end}")

# get average APR from high and low
candles["avg"] = candles[["high", "low"]].mean(axis=1)
avg_apr = Decimal(candles["avg"].mean() / 100)

duration = Decimal((end - start).total_seconds())
accrued_interest_estimation = (1 + 1 * avg_apr) * duration / SECONDS_PER_YEAR # Use Aave v3 seconds per year

assert accrued_interest_estimation >= 1, f"Aave interest cannot be negative: {accrued_interest_estimation}"

return accrued_interest_estimation


@dataclass
class LendingCandleUniverse:
Expand Down Expand Up @@ -706,4 +804,6 @@ def lending_reserves(self) -> LendingReserveUniverse:
if metric:
return metric.reserves

raise AssertionError("Empty LendingCandlesUniverse")
raise AssertionError("Empty LendingCandlesUniverse")


0 comments on commit 19b84b1

Please sign in to comment.