Skip to content

Commit

Permalink
prettify /pool command output, fix problematic pools
Browse files Browse the repository at this point in the history
  • Loading branch information
callmephilip committed Nov 16, 2023
1 parent a7f222c commit 593b882
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 105 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ DISCORD_TOKEN_PRICING=
DISCORD_TOKEN_TVL=
DISCORD_TOKEN_FEES=
DISCORD_TOKEN_REWARDS=
DISCORD_TOKEN_COMMANDER=
WEB3_PROVIDER_URI=https://mainnet.optimism.io
PROTOCOL_NAME=Velodrome
APP_BASE_URL=https://velodrome.finance
LP_SUGAR_ADDRESS=0xa1F09427fa89b92e9B4e4c7003508C8614F19791
PRICE_ORACLE_ADDRESS=0x07F544813E9Fb63D57a92f28FbD3FF0f7136F5cE
PRICE_BATCH_SIZE=40
Expand All @@ -16,5 +18,7 @@ STABLE_TOKEN_ADDRESS=0x7F5c764cBc14f9669B88837ca1490cCa17c31607
BOT_TICKER_INTERVAL_MINUTES=1
# caching for Sugar tokens
SUGAR_TOKENS_CACHE_MINUTES=10
# caching for Sugar LPs
SUGAR_LPS_CACHE_MINUTES=10
# caching for oracle pricing
ORACLE_PRICES_CACHE_MINUTES=10
3 changes: 2 additions & 1 deletion bots/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
DISCORD_TOKEN_TVL,
DISCORD_TOKEN_FEES,
DISCORD_TOKEN_REWARDS,
DISCORD_TOKEN_COMMANDER,
TOKEN_ADDRESS,
STABLE_TOKEN_ADDRESS,
PROTOCOL_NAME,
Expand Down Expand Up @@ -44,7 +45,7 @@ async def main():
fees_bot.start(DISCORD_TOKEN_FEES),
tvl_bot.start(DISCORD_TOKEN_TVL),
rewards_bot.start(DISCORD_TOKEN_REWARDS),
commander_bot.start(DISCORD_TOKEN_TVL),
commander_bot.start(DISCORD_TOKEN_COMMANDER),
)


Expand Down
6 changes: 3 additions & 3 deletions bots/commander.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ async def on_select_pool(
else address_or_pool
)
tvl = await LiquidityPool.tvl([pool])
embed = await PoolStats().render(pool, tvl)

await response.send_message(embed=embed)
await response.send_message(
await PoolStats().render(pool, tvl), suppress_embeds=True
)


@bot.tree.command(name="pool", description="Get data for specific pool")
Expand Down
40 changes: 26 additions & 14 deletions bots/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from web3 import AsyncWeb3, AsyncHTTPProvider
from web3.constants import ADDRESS_ZERO
from dataclasses import dataclass
from typing import Tuple, List, Dict
from typing import Tuple, List, Dict, Optional

from .settings import (
WEB3_PROVIDER_URI,
Expand All @@ -15,6 +15,7 @@
CONNECTOR_TOKENS_ADDRESSES,
STABLE_TOKEN_ADDRESS,
SUGAR_TOKENS_CACHE_MINUTES,
SUGAR_LPS_CACHE_MINUTES,
ORACLE_PRICES_CACHE_MINUTES,
PRICE_BATCH_SIZE,
GOOD_ENOUGH_PAGINATION_LIMIT,
Expand Down Expand Up @@ -53,16 +54,20 @@ def from_tuple(cls, t: Tuple) -> "Token":
@classmethod
@cache_in_seconds(SUGAR_TOKENS_CACHE_MINUTES * 60)
async def get_all_listed_tokens(cls) -> List["Token"]:
tokens = await cls.get_all_tokens()
return list(filter(lambda t: t.listed, tokens))

@classmethod
@cache_in_seconds(SUGAR_TOKENS_CACHE_MINUTES * 60)
async def get_all_tokens(cls) -> List["Token"]:
sugar = w3.eth.contract(address=LP_SUGAR_ADDRESS, abi=LP_SUGAR_ABI)
tokens = await sugar.functions.tokens(
GOOD_ENOUGH_PAGINATION_LIMIT, 0, ADDRESS_ZERO
).call()
return list(
filter(lambda t: t.listed, map(lambda t: Token.from_tuple(t), tokens))
)
return list(map(lambda t: Token.from_tuple(t), tokens))

@classmethod
async def get_by_token_address(cls, token_address: str) -> "Token":
async def get_by_token_address(cls, token_address: str) -> Optional["Token"]:
"""Get details for specific token
Args:
Expand Down Expand Up @@ -238,8 +243,9 @@ def from_tuple(
)

@classmethod
@cache_in_seconds(SUGAR_LPS_CACHE_MINUTES * 60)
async def get_pools(cls) -> List["LiquidityPool"]:
tokens = await Token.get_all_listed_tokens()
tokens = await Token.get_all_tokens()
prices = await Price.get_prices(tokens)

tokens = {t.token_address: t for t in tokens}
Expand All @@ -257,6 +263,7 @@ async def get_pools(cls) -> List["LiquidityPool"]:
)

@classmethod
@cache_in_seconds(SUGAR_LPS_CACHE_MINUTES * 60)
async def by_address(cls, address: str) -> "LiquidityPool":
pools = await cls.get_pools()
try:
Expand Down Expand Up @@ -317,25 +324,29 @@ def volume_pct(self) -> float:

@property
def volume(self) -> float:
return self.volume_pct * (
self.token0_fees.amount_in_stable + self.token1_fees.amount_in_stable
)
t0 = self.token0_fees.amount_in_stable if self.token0_fees else 0
t1 = self.token1_fees.amount_in_stable if self.token1_fees else 0
return self.volume_pct * (t0 + t1)

@property
def token0_volume(self) -> float:
return self.token0_fees.amount * self.volume_pct
return self.token0_fees.amount * self.volume_pct if self.token0_fees else 0

@property
def token1_volume(self) -> float:
return self.token1_fees.amount * self.volume_pct
return self.token1_fees.amount * self.volume_pct if self.token1_fees else 0

def apr(self, tvl: float) -> float:
day_seconds = 24 * 60 * 60
reward_value = self.emissions.amount_in_stable
reward_value = self.emissions.amount_in_stable if self.emissions else 0
reward = reward_value * day_seconds
staked_pct = 100 * self.gauge_total_supply / self.total_supply
staked_pct = (
100 * self.gauge_total_supply / self.total_supply
if self.total_supply != 0
else 0
)
staked_tvl = tvl * staked_pct / 100
return (reward / staked_tvl) * (100 * 365)
return (reward / staked_tvl) * (100 * 365) if staked_tvl != 0 else 0


@dataclass(frozen=True)
Expand All @@ -350,6 +361,7 @@ class LiquidityPoolEpoch:
fees: List[Amount]

@classmethod
@cache_in_seconds(SUGAR_LPS_CACHE_MINUTES * 60)
async def fetch_latest(cls):
tokens = await Token.get_all_listed_tokens()
prices = await Price.get_prices(tokens)
Expand Down
7 changes: 6 additions & 1 deletion bots/helpers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import logging
import os
import sys
import urllib

from typing import List
from typing import List, Dict
from web3 import Web3
from async_lru import alru_cache

Expand Down Expand Up @@ -53,6 +54,10 @@ def amount_to_m_string(amount: float) -> str:
return f"{round(amount/1000000, 2)}M"


def make_app_url(base_url: str, path: str, params: Dict) -> str:
return f"{base_url}{path}?{urllib.parse.urlencode(params)}"


# logging
LOGGING_LEVEL = os.getenv("LOGGING_LEVEL", "DEBUG")
LOGGING_HANDLER = logging.StreamHandler(sys.stdout)
Expand Down
3 changes: 3 additions & 0 deletions bots/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
DISCORD_TOKEN_TVL = os.environ["DISCORD_TOKEN_TVL"]
DISCORD_TOKEN_FEES = os.environ["DISCORD_TOKEN_FEES"]
DISCORD_TOKEN_REWARDS = os.environ["DISCORD_TOKEN_REWARDS"]
DISCORD_TOKEN_COMMANDER = os.environ["DISCORD_TOKEN_COMMANDER"]

WEB3_PROVIDER_URI = os.environ["WEB3_PROVIDER_URI"]
LP_SUGAR_ADDRESS = os.environ["LP_SUGAR_ADDRESS"]
PRICE_ORACLE_ADDRESS = os.environ["PRICE_ORACLE_ADDRESS"]
PRICE_BATCH_SIZE = int(os.environ["PRICE_BATCH_SIZE"])

PROTOCOL_NAME = os.environ["PROTOCOL_NAME"]
APP_BASE_URL = os.environ["APP_BASE_URL"]

# token we are converting from
TOKEN_ADDRESS = normalize_address(os.environ["TOKEN_ADDRESS"])
Expand All @@ -35,6 +37,7 @@

BOT_TICKER_INTERVAL_MINUTES = int(os.environ["BOT_TICKER_INTERVAL_MINUTES"])
SUGAR_TOKENS_CACHE_MINUTES = int(os.environ["SUGAR_TOKENS_CACHE_MINUTES"])
SUGAR_LPS_CACHE_MINUTES = int(os.environ["SUGAR_LPS_CACHE_MINUTES"])
ORACLE_PRICES_CACHE_MINUTES = int(os.environ["ORACLE_PRICES_CACHE_MINUTES"])

GOOD_ENOUGH_PAGINATION_LIMIT = 2000
134 changes: 48 additions & 86 deletions bots/ui/pool_stats.py
Original file line number Diff line number Diff line change
@@ -1,91 +1,53 @@
import discord
from ..data import LiquidityPool
from ..helpers import format_percentage, format_currency
from ..helpers import format_percentage, format_currency, make_app_url
from ..settings import APP_BASE_URL


class PoolStats:
async def render(self, pool: LiquidityPool, tvl: float):
embed = discord.Embed(
title=f"{pool.symbol}",
description=" | ".join(
[
f"{'Stable Pool' if pool.is_stable else 'Volatile Pool'}",
f"Trading fee: {format_percentage(pool.pool_fee_percentage)}",
f"TVL: ~{format_currency(tvl)}",
f"APR: {format_percentage(pool.apr(tvl))}",
]
),
color=0xFFFFFF,
)

embed.add_field(name="", value="", inline=False)

# Volume

embed.add_field(name="Volume", value="", inline=False)
embed.add_field(
name=" ",
value=format_currency(pool.volume),
inline=True,
)
embed.add_field(
name=" ",
value=format_currency(
pool.token0_volume, symbol=pool.token0.symbol, prefix=False
),
inline=True,
)
embed.add_field(
name=" ",
value=format_currency(
pool.token1_volume, symbol=pool.token1.symbol, prefix=False
),
inline=True,
)
embed.add_field(name="", value="", inline=False)

# Fees

embed.add_field(name="Fees", value="", inline=False)
embed.add_field(
name=" ",
value=format_currency(
pool.token0_fees.amount_in_stable + pool.token1_fees.amount_in_stable
),
inline=True,
)
embed.add_field(
name=" ",
value=format_currency(
pool.token0_fees.amount, symbol=pool.token0.symbol, prefix=False
),
inline=True,
)
embed.add_field(
name=" ",
value=format_currency(
pool.token1_fees.amount, symbol=pool.token1.symbol, prefix=False
),
inline=True,
)
embed.add_field(name="", value="", inline=False)

# Pool balance

embed.add_field(name="Pool Balance", value="", inline=False)
embed.add_field(
name=" ",
value=format_currency(
pool.reserve0.amount, symbol=pool.token0.symbol, prefix=False
),
inline=True,
async def render(self, pool: LiquidityPool, tvl: float) -> str:
token0_fees = pool.token0_fees.amount_in_stable if pool.token0_fees else 0
token1_fees = pool.token1_fees.amount_in_stable if pool.token1_fees else 0

template_args = {
"pool_symbol": pool.symbol,
"pool_fee_percentage": format_percentage(pool.pool_fee_percentage),
"apr": format_percentage(pool.apr(tvl)),
"tvl": format_currency(tvl),
"token0_volume": format_currency(
pool.reserve0.amount if pool.reserve0 else 0,
symbol=pool.token0.symbol,
prefix=False,
),
"token1_volume": format_currency(
pool.reserve1.amount if pool.reserve1 else 0,
symbol=pool.token1.symbol,
prefix=False,
),
"volume": format_currency(pool.volume),
"fees": format_currency(token0_fees + token1_fees),
"deposit_url": make_app_url(
APP_BASE_URL,
"/deposit",
{
"token0": pool.token0.token_address,
"token1": pool.token1.token_address,
"stable": str(pool.is_stable).lower(),
},
),
"incentivize_url": make_app_url(
APP_BASE_URL, "/incentivize", {"pool": pool.lp}
),
}

return """
> **{pool_symbol} ● Fee {pool_fee_percentage} ● {apr} APR**
> - ~{tvl} TVL
> - {token0_volume}
> - {token1_volume}
> - ~{volume} volume this epoch
> - ~{fees} fees this epoch
>
> [Deposit 🐖]({deposit_url}) ● [Incentivize 🙋]({incentivize_url})
""".format(
**template_args
)
embed.add_field(
name=" ",
value=format_currency(
pool.reserve1.amount, symbol=pool.token1.symbol, prefix=False
),
inline=True,
)

return embed
25 changes: 25 additions & 0 deletions tests/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,28 @@ async def test_rewards():

assert fees != 0
assert bribes != 0


@pytest.mark.asyncio
async def test_liquidity_pool_stats():
pools = await LiquidityPool.get_pools()
for pool in pools:
tvl = await LiquidityPool.tvl([pool])
fields = [
pool.token0,
pool.token1,
pool.is_stable,
pool.pool_fee_percentage,
pool.apr(tvl),
pool.volume,
pool.token0_volume,
pool.token1_volume,
pool.token0_fees.amount_in_stable if pool.token0_fees else 0,
pool.token1_fees.amount_in_stable if pool.token1_fees else 0,
pool.token0_fees.amount if pool.token0_fees else 0,
pool.token1_fees.amount if pool.token1_fees else 0,
pool.reserve0.amount if pool.reserve0 else 0,
pool.reserve1.amount if pool.reserve1 else 0,
]
for field in fields:
assert field is not None

0 comments on commit 593b882

Please sign in to comment.