Duplicated for ETHWBTC - Initial Summary Tables Identified

This commit is contained in:
Carlos R. Mercado 2022-12-02 15:32:26 -05:00
parent fa0cbd0e17
commit 0abfe4a5df
6 changed files with 549 additions and 9 deletions

View File

@ -154,7 +154,6 @@ saveRDS(filled_eth_prices, "eth_prices.rds")
```{r, message = FALSE, warning = FALSE}
library(gmp) # large numbers
library(reactable) # clean tables
library(plotly) # graphs
library(dplyr) # data manipulation
options(scipen = 10)
@ -1049,7 +1048,7 @@ id_fees_fixed <- lapply(1:length(id_fees_list),
})
# cleanup
rm(closed_lp_actions, closed_fees, closed_lp_actions, id_fees_list)
rm(closed_lp_actions, closed_fees, id_fees_list)
```
@ -1059,12 +1058,6 @@ rm(closed_lp_actions, closed_fees, closed_lp_actions, id_fees_list)
Now with the fixed fee list organized by unique IDs and lp_actions list organized by unique IDs
we can create `r length(id_fees_fixed)` accounting tables.
```{r}
# fix this
accounting(id_lp_actions = id_lp_list[[68]],id_fees = id_fees_fixed[[68]])
```
```{r}
id_accounting <- lapply(1:length(id_fees_fixed),
@ -1107,7 +1100,7 @@ strategy_results <- bind_rows(id_strat, .id = "unique_id")
ethusdc_results <- merge(pnl_results, hodl_results, by = "unique_id")
ethusdc_results <- merge(ethusdc_results, strategy_results, by = "unique_id")
saveRDS(ethusdc_results)
saveRDS(ethusdc_results, file = "ethusdc_results.rds")
reactable(
ethusdc_results[1:20, ]

View File

@ -0,0 +1,91 @@
library(shroomDK)
library(zoo) # infill NAs & rolling Median
source("../key_functions.R")
# LP Actions
ethbtc_lp_actions <- auto_paginate_query(
query = "
SELECT
BLOCK_NUMBER, BLOCK_TIMESTAMP,
TX_HASH, ACTION,
NF_TOKEN_ID,
AMOUNT0_ADJUSTED, AMOUNT1_ADJUSTED,
LIQUIDITY,
TOKEN0_SYMBOL, TOKEN1_SYMBOL,
TICK_LOWER, TICK_UPPER,
PRICE_LOWER_0_1, PRICE_UPPER_0_1,
LIQUIDITY_PROVIDER,
NF_POSITION_MANAGER_ADDRESS
FROM ethereum.uniswapv3.ez_lp_actions
WHERE POOL_ADDRESS = '0xcbcdf9626bc03e24f779434178a73a0b4bad62ed' AND
BLOCK_NUMBER <= 15576600
ORDER BY BLOCK_NUMBER DESC
",
api_key = readLines("api_key.txt")
)
# Collected Fees
# Known Issue where Closure of positions mix withdrawn tokens as if they were collected fees
# subtraction will be sorted out
ethbtc_fees <- auto_paginate_query(
query = "
SELECT
BLOCK_NUMBER, BLOCK_TIMESTAMP,
TX_HASH, NF_TOKEN_ID,
AMOUNT0_ADJUSTED, AMOUNT1_ADJUSTED,
TICK_LOWER, TICK_UPPER,
PRICE_LOWER, PRICE_UPPER,
LIQUIDITY_PROVIDER,
NF_POSITION_MANAGER_ADDRESS
FROM ethereum.uniswapv3.ez_position_collected_fees
WHERE POOL_ADDRESS = '0xcbcdf9626bc03e24f779434178a73a0b4bad62ed' AND
BLOCK_NUMBER <= 15576600
ORDER BY BLOCK_NUMBER DESC
",
api_key = readLines("api_key.txt")
)
# Historical BTC prices at all LP_ACTIONS blocks
min_block = min(ethbtc_lp_actions$BLOCK_NUMBER) - 101
blocks = c(min_block, min_block + 1e6,
min_block + 2e6, min_block + 3e6,
min_block + 4e6)
ethbtc_price <- list()
for(i in 1:length(blocks)){
ethbtc_price[[i]] <- get_ethbtc_price(min_block = blocks[i],
max_block = blocks[i] + 1e6,
api_key = readLines('api_key.txt'))
}
all_ethbtc_prices <- do.call(rbind, ethbtc_price)
all_ethbtc_prices <- all_ethbtc_prices[order(all_ethbtc_prices$BLOCK_NUMBER),]
# if a block has no trades, infill the BLOCK_NUMBER and persist the most recent
# ETH Weighted Average Price, with 0 VOLUME and 0 NUM_SWAPS
infill <- data.frame(
BLOCK_NUMBER = min(all_ethbtc_prices$BLOCK_NUMBER):max(all_ethbtc_prices$BLOCK_NUMBER)
)
filled_ethbtc_prices <- merge(all_ethbtc_prices, infill, all.x = TRUE, all.y = TRUE)
filled_ethbtc_prices[is.na(filled_ethbtc_prices$"BTC_VOLUME"), c("BTC_VOLUME","NUM_SWAPS")] <- 0
# Improves analysis speed to front-load these calculations and is more smoothed
filled_ethbtc_prices$BTC_WAVG_PRICE <- zoo::na.locf(filled_ethbtc_prices$BTC_WAVG_PRICE)
BTC_MARKET_PRICE <- zoo::rollmedian(x = filled_ethbtc_prices$BTC_WAVG_PRICE, k = 99, align = "left")
diff_median <- nrow(filled_ethbtc_prices) - length(BTC_MARKET_PRICE)
BTC_MARKET_PRICE <- c(filled_ethbtc_prices$BTC_WAVG_PRICE[1:diff_median], BTC_MARKET_PRICE)
filled_ethbtc_prices$BTC_MARKET_PRICE <- BTC_MARKET_PRICE
# R Save Format
saveRDS(ethbtc_lp_actions, "ethbtc_lp_actions.rds")
saveRDS(ethbtc_fees, "ethbtc_fees.rds")
saveRDS(filled_ethbtc_prices, "ethbtc_prices.rds")

View File

@ -0,0 +1,286 @@
---
title: "eth_btc_retroactive"
author: "Charliemarketplace"
date: "`r Sys.Date()`"
output:
html_document:
code_folding: hide
toc: yes
toc_float:
collapsed: false
editor_options:
chunk_output_type: console
---
# Intro
This is a Profit and Loss calculation including potential divergent loss (DL) of all closed positions in the ETH-WBTC 0.3% Uniswap v3 pool from inception of the pool up to the block height of 15,576,600 (September 20, 2022). It reviews *all* changes in liquidity and accumulated fees to identify the profit and loss of every unique position across 3 accountings:
- Direct Profit & Loss, accounting for price changes in the underlying assets
- Opportunity Cost, i.e., HODL Reference Value: what assets deposited would have been worth as of the block where the position was closed.
- Realized Value, i.e., Strategy Reference Value: what the assets withdrawn were worth as of the block where the position was closed including accumulated fees.
Direct P&L is useful for understanding gains in the context of changing prices, here, P&L is measured in both ETH and BTC terms.
Divergent Loss (sometimes called Impermanent Loss) is the difference between the results of using a strategy (here, fees and automatic market making in Uniswap v3) and the counter factual: the amount of assets if one were *not* to have used the strategy.
A more in depth view on definitions and calculations is available in the ETH USDC Retroactive over the same time range.
# Data Collection
The `collect_ethbtc_data.R` script included in this repo uses the Flipside Crypto's shroomDK API to pull all the relevant data. For brevity, this markdown reads from a saved RDS copy *not* available in the repo. To reproduce this analysis, you can run the `collect_ethbtc_data.R` script using your own shroomDK API key available for free.
```{r}
library(gmp) # large numbers
library(reactable) # clean tables
library(dplyr) # data manipulation
options(scipen = 10)
source("../key_functions.R")
lp_actions <- readRDS("ethbtc_lp_actions.rds")
# remove actions that don't remove liquidity, these are fee collection events
lp_actions <- lp_actions %>% filter(LIQUIDITY != 0)
fees <- readRDS("ethbtc_fees.rds")
btc_prices <- readRDS("ethbtc_prices.rds")
btc_prices <- btc_prices[,c("BLOCK_NUMBER", "BTC_MARKET_PRICE")]
colnames(btc_prices) <- c("BLOCK_NUMBER","btc_price")
```
### Closed Positions at Block Height
```{r}
lp_actions$unique_id <- lp_actions$NF_TOKEN_ID
custom_index <- which(is.na(lp_actions$unique_id))
lp_actions[custom_index, "unique_id"] <- paste0(lp_actions[custom_index, "NF_POSITION_MANAGER_ADDRESS"],
"--",
lp_actions[custom_index, "TICK_LOWER"],
"--",
lp_actions[custom_index, "TICK_UPPER"])
# adding BTC Market Price from btc_prices to lp_actions via merge for later
lp_actions <- merge(lp_actions, y = btc_prices, all.x = TRUE, all.y = FALSE, by = "BLOCK_NUMBER")
# manual insertion of first price
lp_actions$btc_price[is.na(lp_actions$btc_price)] <- 16.07226
# while here do fees too
fees$unique_id <- fees$NF_TOKEN_ID
custom_index <- which(is.na(fees$unique_id))
fees[custom_index,"unique_id"] <- paste0(fees[custom_index, "NF_POSITION_MANAGER_ADDRESS"],
"--",
fees[custom_index, "TICK_LOWER"],
"--",
fees[custom_index, "TICK_UPPER"])
# also while here, ensure only 1 fee collection row per tx_hash, i.e., just add them
fees <- fees %>%
group_by(across(c(-AMOUNT0_ADJUSTED, -AMOUNT1_ADJUSTED))) %>%
summarise(
AMOUNT0_ADJUSTED = sum(AMOUNT0_ADJUSTED),
AMOUNT1_ADJUSTED = sum(AMOUNT1_ADJUSTED), .groups = "drop") %>%
relocate(c(NF_TOKEN_ID, AMOUNT0_ADJUSTED, AMOUNT1_ADJUSTED), .after = NF_TOKEN_ID) %>%
as.data.frame()
liquidity_at_timestamp <- lp_actions %>% mutate(
liquidity_signed = ifelse(ACTION == "DECREASE_LIQUIDITY", LIQUIDITY * -1, LIQUIDITY)
) %>% group_by(unique_id) %>%
summarise(sumliq = sum(liquidity_signed))
# due to some large number precision errors, some liquidity may be technically negative
# but effectively 0 for our purposes.
closed_positions <- liquidity_at_timestamp %>% filter(sumliq <= 0)
```
There are `r nrow(closed_positions)` ETH-BTC 0.3% closed positions, including vaults, as of Sept 20, 2022.
## Functions
```{r}
price_col = "btc_price"
pnl <- function(id_accounting, t0col, t1col, price_col, price_base = "t1/t0"){
# allow user to choose the base that is easier to read
# e.g., usdc/eth may be easier to read than eth/usdc; which means t0/t1 instead of t1/t0
# while ETH/BTC may be easier than BTC/ETH; which is t1/t0 the default!
if(price_base == 't1/t0'){
pnl = data.frame(
pnl_t0_terms = sum(id_accounting$token0, (id_accounting$token1/id_accounting[[price_col]])),
pnl_t1_terms = sum(id_accounting$token1, (id_accounting$token0*id_accounting[[price_col]]))
)
} else if(price_base == 't0/t1'){
pnl = data.frame(
pnl_t0_terms = sum(id_accounting$token0, (id_accounting$token1*id_accounting[[price_col]])),
pnl_t1_terms = sum(id_accounting$token1, (id_accounting$token0/id_accounting[[price_col]]))
)
} else {
stop("price_base must be 't1/t0' (default) or 't0/t1'")
}
colnames(pnl) <- c(t0col, t1col)
return(pnl)
}
hodl_reference <- function(id_accounting, t0col, t1col, price_col, price_base = "t1/t0"){
price_at_close_block = id_accounting[[price_col]][id_accounting$BLOCK_NUMBER == max(id_accounting$BLOCK_NUMBER)][1]
if(price_base == 't1/t0'){
hodl <- id_accounting %>%
filter(accounting == 'cost_basis') %>%
summarise(
hodl_t0_terms = abs(sum(token0) + sum(token1 / price_at_close_block)),
hodl_t1_terms = abs(sum(token1) + sum(token0 * price_at_close_block)),
)
} else if(price_base == 't0/t1'){
hodl <- id_accounting %>%
filter(accounting == 'cost_basis') %>%
summarise(
hodl_t0_terms = abs(sum(token0) + sum(token1 * price_at_close_block)),
hodl_t1_terms = abs(sum(token1) + sum(token0 / price_at_close_block)),
)
} else {
stop("price_base must be 't1/t0' (default) or 't0/t1'")
}
colnames(hodl) <- c(t0col, t1col)
return(hodl)
}
strategy_reference <- function(id_accounting, t0col, t1col, price_col, price_base = "t1/t0"){
price_at_close_block = id_accounting[[price_col]][id_accounting$BLOCK_NUMBER == max(id_accounting$BLOCK_NUMBER)][1]
if(price_base == 't1/t0'){
strat <- id_accounting %>%
filter(accounting != 'cost_basis') %>%
summarise(
strategy_t0_terms = abs(sum(token0) + sum(token1 / price_at_close_block)),
strategy_t1_terms = abs(sum(token1) + sum(token0 * price_at_close_block)),
)
} else if(price_base == 't0/t1'){
strat <- id_accounting %>%
filter(accounting != 'cost_basis') %>%
summarise(
strategy_t0_terms = abs(sum(token0) + sum(token1 * price_at_close_block)),
strategy_t1_terms = abs(sum(token1) + sum(token0 / price_at_close_block)),
)
} else {
stop("price_base must be 't1/t0' (default) or 't0/t1'")
}
colnames(strat) <- c(t0col, t1col)
return(strat)
}
```
# Fee Correction
```{r}
closed_fees <- fees %>%
filter(unique_id %in% closed_positions$unique_id) %>%
arrange(BLOCK_NUMBER)
closed_lp_actions <- lp_actions %>%
filter(unique_id %in% closed_positions$unique_id)
closed_lp_w_fees <- closed_lp_actions %>% filter(
unique_id %in% closed_fees$unique_id
) %>% arrange(BLOCK_NUMBER) %>%
select(BLOCK_NUMBER, TX_HASH, unique_id, ACTION, AMOUNT0_ADJUSTED, AMOUNT1_ADJUSTED, btc_price)
# split automatically alphabetizes by ID #
id_fees_list <- split(closed_fees, f = closed_fees$unique_id)
id_lp_list <- split(closed_lp_w_fees, f = closed_lp_w_fees$unique_id)
# If the order of IDs don't exactly match note the problem
if(
! identical(names(id_lp_list), names(id_fees_list))
) {
stop("List of IDs are not aligned")
}
# otherwise run through the list of fees and correct them
# NOTE fee_correction only safe to use when split by unique_id
# Possible for a tx_hash to include multiple position interactions
id_fees_fixed <- lapply(1:length(id_fees_list),
FUN = function(i){
fee_correction(id_fees_list[[i]], id_lp_list[[i]])
})
```
### Accounting
```{r}
aa <- accounting(id_lp_actions = id_lp_list[[100]],
id_fees = id_fees_fixed[[100]],
price_col = "btc_price")
pnl(aa, "pnl_btc_terms", "pnl_eth_terms","btc_price")
hodl_reference(aa, "hodl_btc_terms", "hodl_eth_terms","btc_price")
strategy_reference(aa, "strat_btc_terms", "strat_eth_terms","btc_price")
```
```{r}
id_accounting <- lapply(1:length(id_lp_list),
FUN = function(i){
tryCatch(
accounting(id_lp_actions = id_lp_list[[i]],
id_fees = id_fees_fixed[[i]],price_col = "btc_price"),
error = function(e){
print(paste0("Error found in position: ", i))
})
})
names(id_accounting) <- names(id_lp_list)
# to simplify segmentation of analysis saving this
saveRDS(id_accounting, "post_ethbtc_accounting.rds")
# for brevity read id_accounting to continue here
id_accounting <- readRDS("post_ethbtc_accounting.rds")
id_pnl <- lapply(id_accounting, function(i){
pnl(i, "pnl_btc_terms", "pnl_eth_terms","btc_price")
})
pnl_results <- bind_rows(id_pnl, .id = "unique_id")
id_hodl <- lapply(id_accounting, function(i){
hodl_reference(i, "hodl_btc_terms", "hodl_eth_terms","btc_price")
})
hodl_results <- bind_rows(id_hodl, .id = "unique_id")
id_strat <- lapply(id_accounting, function(i){
strategy_reference(i, "strat_btc_terms", "strat_eth_terms","btc_price")
})
strategy_results <- bind_rows(id_strat, .id = "unique_id")
ethbtc_results <- merge(pnl_results, hodl_results, by = "unique_id")
ethbtc_results <- merge(ethbtc_results, strategy_results, by = "unique_id")
saveRDS(ethbtc_results, file = "ethbtc_results.rds")
```

View File

@ -0,0 +1,20 @@
library(dplyr)
options(scipen = 10)
ethbtc_results <- readRDS("ethbtc/ethbtc_results.rds")
n = nrow(ethbtc_results)
table(ethbtc_results$pnl_btc_terms > 0)/n
table(ethbtc_results$pnl_eth_terms > 0)/n
table(ethbtc_results$pnl_btc_terms > 0 & ethbtc_results$pnl_eth_terms > 0)/n
table(ethbtc_results$strat_btc_terms > ethbtc_results$hodl_btc_terms)/n
table(ethbtc_results$strat_btc_terms > ethbtc_results$hodl_btc_terms &
ethbtc_results$strat_eth_terms > ethbtc_results$hodl_eth_terms)/n

View File

@ -0,0 +1,20 @@
library(dplyr)
options(scipen = 10)
ethusdc_results <- readRDS("ethusdc_results.rds")
n = nrow(ethusdc_results)
table(ethusdc_results$pnl_usd_terms > 0)/n
table(ethusdc_results$pnl_eth_terms > 0)/n
table(ethusdc_results$pnl_usd_terms > 0 & ethusdc_results$pnl_eth_terms > 0)/n
table(ethusdc_results$strategy_usd_terms > ethusdc_results$hodl_usd_terms)/n
table(ethusdc_results$strategy_usd_terms > ethusdc_results$hodl_usd_terms &
ethusdc_results$strategy_eth_terms > ethusdc_results$hodl_eth_terms)/n

View File

@ -1,5 +1,90 @@
library(gmp) # big int math
fee_correction <- function(id_fees, id_lp_actions){
# fee-collection problem only occurs in decrease_liquidity
# sometimes in the same transaction the same exact liquidity is added and immediately removed
# occurs in poorly written NF_POSITION_MANAGERS.
id_lp_actions <- id_lp_actions %>% filter(ACTION == "DECREASE_LIQUIDITY")
fee_tx_in_lp = (id_fees$TX_HASH %in% id_lp_actions$TX_HASH)
lp_tx_in_fee = (id_lp_actions$TX_HASH %in% id_fees$TX_HASH)
amount_cols = c("AMOUNT0_ADJUSTED", "AMOUNT1_ADJUSTED")
# if there are no tx hashes in both tables move on
if(sum(fee_tx_in_lp) == 0){
return(id_fees)
} else {
# if it perfectly lines up that ALL tx hashes in
# both tables have fees collected double counting withdrawals;
# fix by subtracting the double counting
# note: it is assumed transactions are ordered by block number for alignment to work
if(sum(fee_tx_in_lp) == sum(lp_tx_in_fee)){
if(mean(id_fees[fee_tx_in_lp, amount_cols] >= id_lp_actions[lp_tx_in_fee, amount_cols]) == 1){
id_fees[fee_tx_in_lp, amount_cols] <- {
id_fees[fee_tx_in_lp, amount_cols] - id_lp_actions[lp_tx_in_fee, amount_cols]
}
}
} else {
# else go 1 by one to overlapping tx and check again
for(i in id_fees$TX_HASH[fee_tx_in_lp]){
if(
mean(
id_fees[id_fees$TX_HASH == i, amount_cols] >=
id_lp_actions[id_lp_actions$TX_HASH == i, amount_cols]
) == 1
){
id_fees[id_fees$TX_HASH == i, amount_cols] <- {
id_fees[id_fees$TX_HASH == i, amount_cols] -
id_lp_actions[id_lp_actions$TX_HASH == i, amount_cols]
}
} else next()
}
}
return(id_fees)
}
}
accounting <- function(id_lp_actions, id_fees, price_col){
# cost basis negative for subtraction
cost_basis <- id_lp_actions %>% filter(ACTION == "INCREASE_LIQUIDITY") %>%
mutate(
token0 = -1*AMOUNT0_ADJUSTED,
token1 = -1*AMOUNT1_ADJUSTED,
accounting = "cost_basis"
) %>%
select(BLOCK_NUMBER, accounting, token0, token1, all_of(price_col))
withdrawals <- id_lp_actions %>% filter(ACTION == "DECREASE_LIQUIDITY") %>%
mutate(
token0 = AMOUNT0_ADJUSTED,
token1 = AMOUNT1_ADJUSTED,
accounting = "withdrawal"
) %>%
select(BLOCK_NUMBER, accounting, token0, token1, all_of(price_col))
fees <- data.frame(
BLOCK_NUMBER = max(withdrawals$BLOCK_NUMBER),
accounting = "fee revenue",
token0 = sum(id_fees$AMOUNT0_ADJUSTED),
token1 = sum(id_fees$AMOUNT1_ADJUSTED)
)
# price fees at position closure, i.e., last withdrawal
fees[[price_col]] <- tail(withdrawals[[price_col]], n = 1)
accounting_tbl <- rbind(cost_basis, withdrawals, fees)
return(accounting_tbl)
}
get_eth_price <- function(min_block, max_block, api_key){
price_query <- {
"
@ -49,6 +134,51 @@ ORDER BY BLOCK_NUMBER DESC
prices = auto_paginate_query(price_query, api_key = api_key)
}
get_ethbtc_price <- function(min_block, max_block, api_key){
price_query <- {
"
with uniswap_btc_eth_swaps AS (
SELECT * FROM ethereum.uniswapv3.ez_swaps WHERE POOL_ADDRESS IN (
'0xcbcdf9626bc03e24f779434178a73a0b4bad62ed', -- wbtc ETH 0.3%
'0x4585fe77225b41b697c938b018e2ac67ac5a20c0' -- wbtc ETH 0.05%
) AND
BLOCK_NUMBER >= _min_block_ AND
BLOCK_NUMBER <= _max_block_
),
btc_eth_price AS (
SELECT BLOCK_NUMBER, BLOCK_TIMESTAMP,
IFF(TOKEN1_SYMBOL = 'WBTC', ABS(DIV0(AMOUNT0_ADJUSTED, AMOUNT1_ADJUSTED)),
ABS(DIV0(AMOUNT1_ADJUSTED, AMOUNT0_ADJUSTED))
) as btc_eth_trade_price,
IFF(TOKEN1_SYMBOL = 'WBTC', ABS(AMOUNT1_ADJUSTED), ABS(AMOUNT0_ADJUSTED)) as btc_volume,
IFF(TOKEN1_SYMBOL = 'WBTC', TOKEN0_SYMBOL, TOKEN1_SYMBOL) as eth
FROM uniswap_btc_eth_swaps
WHERE ABS(AMOUNT0_ADJUSTED) > 1e-8 AND ABS(AMOUNT1_ADJUSTED) > 1e-8
),
btc_block_price AS (
SELECT BLOCK_NUMBER, BLOCK_TIMESTAMP,
div0(SUM(btc_eth_trade_price * btc_volume),sum(btc_volume)) as btc_wavg_price,
SUM(btc_volume) as btc_volume,
COUNT(*) as num_swaps
FROM btc_eth_price
GROUP BY BLOCK_NUMBER, BLOCK_TIMESTAMP
)
SELECT * FROM btc_block_price
ORDER BY BLOCK_NUMBER DESC
"
}
price_query <- gsub("_min_block_", min_block, price_query)
price_query <- gsub("_max_block_", max_block, price_query)
prices = auto_paginate_query(price_query, api_key = api_key)
}
market_eth_price_at_block <- function(eth_prices, block){
eth_prices[eth_prices$BLOCK_NUMBER == block, "ETH_MARKET_PRICE"]
}