<repo_name>markusbkoch/balancer-apy<file_sep>/bal_apy.py
import pandas as pd
import requests
import json
from web3 import Web3
import os
import sys
THEGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer'
try:
    web3_provider = os.environ['ENDPOINT_URL']
    w3 = Web3(Web3.WebsocketProvider(web3_provider))
except:
    sys.exit('You must set the ENDPOINT_URL environment variable')

def query_thegraph(query):
    r = requests.post(THEGRAPH_URL, json = {'query':query})
    try:
        return json.loads(r.content)['data']
    except:
        print(query)
        print(r.content)
        raise


def get_pool_data(pools, block=0):
    if type(pools) == str:
        pools = [pools.lower()]
    else:
        pools = list(map(lambda x: x.lower(), pools))
    if block==0:
        query = '''
        query {{
            pools(first: 1000, 
                    where: {{id_in: ["{0}"]}}) {{
                id
                liquidity
                totalSwapFee
                totalShares
            }}
        }}'''.format('","'.join(pools))
    else:
        query = '''
        query {{
            pools(first: 1000, block: {{number: {1}}},
                    where: {{id_in: ["{0}"]}}) {{
                id
                liquidity
                totalSwapFee
                totalShares
            }}
        }}'''.format('","'.join(pools), block)
    
    p = query_thegraph(query)['pools']
    df = pd.DataFrame(p).set_index('id').astype(float)
    return df



def get_pools_APY(pools, end_block=None):
    if end_block:
        m = end_block
    else:
        m = w3.eth.getBlock('latest').number - 10 #can't use the actual latest because Subgraph lags behind
    n = m-6600
    t_m = w3.eth.getBlock(m).timestamp
    t_n = w3.eth.getBlock(n).timestamp
    m_data = get_pool_data(pools, m)#.set_index('id')
    n_data = get_pool_data(pools, n)#.set_index('id')
    merge = n_data.join(m_data, lsuffix='_n', rsuffix='_m')
    merge.drop(columns=['liquidity_n','totalShares_n'], inplace=True)
    merge['APY'] = 31556926 / (t_m - t_n) * (merge['totalSwapFee_m'] - merge['totalSwapFee_n']) / merge['liquidity_m']
    return merge[['liquidity_m','totalShares_m','APY']]




def get_pools(liquidity_provider, block=0):
    liquidity_provider = liquidity_provider.lower()
    if block==0:
        query = '''
        query {{
            users(first: 1000, 
                    where: {{id: "{0}"}}) {{
                sharesOwned (where: {{balance_gt: 0}}) {{
                    poolId {{id}}
                    balance
                }}
            }}
        }}'''.format(liquidity_provider)
    else:
        query = '''
        query {{
            users(first: 1000, block: {{number: {1}}},
                    where: {{id: "{0}"}}) {{
                sharesOwned (where: {{balance_gt: 0}}) {{
                    poolId {{id}}
                    balance
                }}
            }}
        }}'''.format(liquidity_provider, block)
    
    balances = query_thegraph(query)['users'][0]['sharesOwned']
    balances = list(map(lambda x: (x['poolId']['id'], x['balance']), balances))
    df = pd.DataFrame(balances)
    df.columns = ['id', 'balance']
    df = df.set_index('id').astype(float)
    return df



def get_lp_APY(lp, block=0):
    if block==0:
        m = w3.eth.getBlock('latest').number - 10
    else:
        m = block
    lp_pools = get_pools(lp, m)
    lp_pools_apy = get_pools_APY(lp_pools.index.values, m)
    merge = lp_pools_apy.join(lp_pools)
    merge['lp_liquidity'] = merge['liquidity_m'] * merge['balance'] / merge['totalShares_m']
    lp_apy = (merge['lp_liquidity'] * merge['APY']).sum() / merge['lp_liquidity'].sum()
    return lp_apy

if sys.argv[1] == 'pool':
    print(f'APY for pool {sys.argv[2]}')
    a = get_pools_APY(sys.argv[2]).loc[sys.argv[2],'APY']
    print('{:.2%}'.format(a))
if sys.argv[1] == 'lp':
    print(f'APY for LP {sys.argv[2]}')
    a = get_lp_APY(sys.argv[2])
    print('{:.2%}'.format(a))

<file_sep>/explainer.md
# Computing APY

## Pool APY
### Approach
* compute the pool's revenue over the past 24h and assume it remains constant for the next 365 days;
* assume liquidity remains constant for the next 365 days; 
* APY is the ratio between the 365 days expected revenue and the liquidity

### Formulas and data sources
$A_m$: Expected future APY at block $m$  
$F_m$: `totalSwapFee` at block $m$  
$F_n$: `totalSwapFee` at block $n$  
$L_m$: `liquidity` at block $m$  
$L_n$: `liquidity` at block $n$  
$t_m$: `timestamp` of block $m$  
$t_n$: `timestamp` of block $n$  

1. Read $m$ from web3
2. Define $n=m-6500$ (roughly 24h worth of blocks)
3. Read $t_m$ and $t_n$ from web3

$$ A_m = \frac{31556926}{t_m - t_n}\cdot\frac{F_m-F_n}{L_m} $$

Alternatively, we could explore the pros and cons of an estimator of the average liquidity over the , eg. the average between the snapshot blocks
 
$$ A_m = \frac{31556926}{t_m - t_n}\cdot\ \frac{F_m-F_n}{\frac{L_m+L_n}{2}} $$

## Liquidity Provider's APY
### Approach
* compute the pool's APY for each pool the LP has liquidity in
* compute the share of the pool held by the LP
* The LP's APY is the weighted arithmetic mean of the APY of the pools they provided liquidity in (weights are the LP's liquidity in each pool)

### Formulas and data sources
$A_{l,m}$: Expected future APY of LP's $l$ position at block $m$  
$A_{p,m}$: Expected future APY of pool $p$ at block $m$  
$L_{p,m}$: `liquidity` of pool $p$ at block $m$  
$L_{l,p,m}$: liquidity provided by LP $l$ at pool $p$ at block $m$  
$S_{p,m}$: `totalShares` of pool $p$ at block $m$  
$B_{l,p,m}$: `balance` of `PoolShare` for LP $l$ at pool $p$ at block $m$  

1. Given an LP $X$, find the set of pools $P$ such that $B_{X,p,m}>0$ (query subgraph `users`)

2. For all pools in $P$, compute $L_{X,p,m}$ and $A_{p,m}$   
$$ L_{X,p,m} = \frac{B_{X,p,m}L_{p,m}}{S_{p,m}} \forall{p\in{P}}$$   
$$ A_{p,m} = \frac{31556926}{t_m - t_n}\cdot\frac{F_{p,m}-F_{p,n}}{L_{p,m}} \forall{p\in{P}}$$

3. Compute $A_{X,m}$  
$$ A_{X,m} = \frac{\sum\limits_{p\in{P}}{A_{p,m}L_{X,p,m}}}{\sum{L_{X,p,m}}} $$<file_sep>/README.md
# Balancer APY
Simple script to compute APY of a pool or a liquidity provider's portfolio.

## Requirements
* Python 3
* An ethereum node (for querying blocks timestamps) 

## Setup
* Install required packages: `pip install -r requirements.txt`
* Configure environment variable:
  * `ENDPOINT_URL`: URL to an ethereum node that can be queried via Websockets
  
## Usage
### Pool APY
`python3 bal_apy.py pool <pool_address>`
### Liquidity Provider's APY
`python3 bal_apy.py lp <liquidity_provider_address>`