From fa54d9edc7a73c4ce8cd66dd8ff71ecc1bb08f17 Mon Sep 17 00:00:00 2001 From: "Carlos R. Mercado" <107061601+charlieflipside@users.noreply.github.com> Date: Mon, 23 Jan 2023 17:36:20 -0500 Subject: [PATCH] V1 --- .gitignore | 1 + onchain-pricing.Rmd | 444 +++++++++++++++++++++++++++++++++++++++++ renv.lock | 471 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 916 insertions(+) diff --git a/.gitignore b/.gitignore index 8f4d207..2f65152 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .RData .Ruserdata api_key.txt +onchain-pricing.html diff --git a/onchain-pricing.Rmd b/onchain-pricing.Rmd index 5057d00..94dd877 100644 --- a/onchain-pricing.Rmd +++ b/onchain-pricing.Rmd @@ -16,4 +16,448 @@ editor_options: FlipsideCrypto's Research is open source. Check out all the code for this report [here](https://github.com/FlipsideCrypto/onchain-pricing) on github. + +```{r setup, include=FALSE} +knitr::opts_chunk$set(warning = FALSE, message = FALSE) +``` + # Intro + +For a deeper dive into AMMs, I recommend reviewing the [Uniswap v2 Explainer](https://science.flipsidecrypto.xyz/uni_v2_explained) which dives into the most popular/forked/copied AMM model +in crypto and its math and key vocabulary: slippage and price impact. + +This report details an on-chain pricing methodology for historical *block level* pricing. Historical data on prices is available from central exchanges and 3rd parties like Coingecko. While point in time prices can be accessed on chain with oracles like Chainlink or pool-specific price reads from Decentralized Exchanges like Uniswap. + +In very small time periods (e.g., minutes) prices can vary widely across exchanges and pools for the same pair of assets. For most use cases, a single price for a slightly longer time period is fine. Chainlink for example updates ETH's USD price on-chain once per hour, unless it detects a significant change in its blend of off-chain and on-chain price feeds, e.g., > 0.5% change triggers an early update. + +Volatility (large price changes in a short amount of time) can make historical analysis difficult. This methodology seeks to combine the best of blending (using multiple price sources) with the best of on-chain access (the calculation is consistent and reproducible without needing off-chain data) to recommend a useful mechanism for assigning a price to a single block for historical analysis. + +The motivation is 3-fold: + +1. Historical Prices should be usable for analysis of small time-windows, e.g., < 60 minutes. +2. Historical Prices should be "realistic", that is, we should believe that our price *could have been* realized somewhere in the ecosystem within the small time-window. +3. Historical Prices should be smooth in normal times, but reactive to sustained volatility that did occur. + +# Methodology + +## 6 Uniswap Pools + +Using 6 Uniswap ETH-Stablecoin pools across fee tiers to get trades in our time period of interest, Block 15,000,000 (2022-06-21) to Block 16,000,000 (2022-11-18). + +- ETH-USDC 0.3% +- ETH-USDC 0.05% +- ETH-USDT 0.3% +- ETH-USDT 0.05% +- ETH-USDC 1.0% +- ETH-DAI 0.3% + +While stablecoins have their own markets and price deviations from each other, we will treat each as equivalent. +1 USDC = 1 USDT = 1 DAI. + +We use a simple trade price of # of ETH in/out vs # stablecoin out/in. Note: the more expensive the trade (i.e., the +higher the fee) the lower volume we should expect in that pool, all else equal. + +The main reason someone would trade (or be routed to) a more expensive pool is if that pool has a relatively better price than the less expensive pool. If ETH-USDC 0.05% price is 0.5% more expensive than the ETH-USDC 0.3% pool for your desired trade at a point in time, paying that 0.25% fee difference makes sense! + +Note: We also treat ETH and wrapped ETH (wETH) equivalently to get a single `eth_stable_trade_price` and measure +our volume in ETH terms. + +Instead of adjusting out fee differences, we use volume-weighting to bias our average price at a block to the pool that experiences more trade volume. + +## Volume-Weighted Average Price + +```{r} +library(shroomDK) +library(plotly) +library(zoo) +library(reactable) +library(dplyr) + +trades_query <- { + " + with uniswap_ETH_stable_swaps AS ( +SELECT * FROM ethereum.uniswapv3.ez_swaps +WHERE +POOL_ADDRESS IN ( +'0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8', -- ETH USDC 0.3% +'0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640', -- ETH USDC 0.05% +'0x4e68ccd3e89f51c3074ca5072bbac773960dfa36', -- ETH USDT 0.3% +'0x11b815efb8f581194ae79006d24e0d814b7697f6', -- ETH USDT 0.05% +'0x7bea39867e4169dbe237d55c8242a8f2fcdcc387', -- ETH USDC 1% +'0xc2e9f25be6257c210d7adf0d4cd6e3e881ba25f8' -- ETH DAI 0.3% +) AND BLOCK_NUMBER >= 15000000 +AND BLOCK_NUMBER <= 16000000 + ), + +eth_stable_price AS ( +SELECT BLOCK_NUMBER, BLOCK_TIMESTAMP, +IFF(TOKEN1_SYMBOL = 'WETH', + ABS(DIV0(AMOUNT0_ADJUSTED, AMOUNT1_ADJUSTED)), + ABS(DIV0(AMOUNT1_ADJUSTED, AMOUNT0_ADJUSTED))) as eth_stable_trade_price, + IFF(TOKEN1_SYMBOL = 'WETH', + ABS(AMOUNT1_ADJUSTED), + ABS(AMOUNT0_ADJUSTED)) as eth_volume, +IFF(TOKEN1_SYMBOL = 'WETH', + TOKEN0_SYMBOL, + TOKEN1_SYMBOL) as stable + FROM uniswap_eth_stable_swaps + WHERE ABS(AMOUNT0_ADJUSTED) > 1e-8 AND ABS(AMOUNT1_ADJUSTED) > 1e-8 +), + +eth_block_price AS ( +SELECT BLOCK_NUMBER, BLOCK_TIMESTAMP, + div0(SUM(eth_stable_trade_price * eth_volume),sum(eth_volume)) as eth_wavg_price, + SUM(eth_volume) as eth_volume, + COUNT(*) as num_swaps + FROM eth_stable_price + GROUP BY BLOCK_NUMBER, BLOCK_TIMESTAMP +) + +SELECT * FROM eth_block_price +ORDER BY BLOCK_NUMBER ASC + " + } + +trades_pull <- auto_paginate_query(query = trades_query, api_key = readLines("api_key.txt")) +reactable(trades_pull[1:10,]) +``` + +Prior to the merge, Ethereum blocks were made roughly every 12-15 seconds. After the merge, +it is a more precise 12 seconds with rare deviation. + +This speed may result in blocks with no relevant trades occuring. For example, blocks +15,000,002; 15,000,006-15,000,008; AND 15,000,010 did not have any trades to our 6 pools. + +## Fill Blocks without Trades + +We can infill these missing blocks (ignoring timestamp), note them as having 0 volume and swaps. + +```{r} + +# 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(trades_pull$BLOCK_NUMBER):max(trades_pull$BLOCK_NUMBER) +) + +filled_eth_prices <- merge(trades_pull, infill, all.x = TRUE, all.y = TRUE) + +filled_eth_prices[is.na(filled_eth_prices$"ETH_VOLUME"), c("ETH_VOLUME","NUM_SWAPS")] <- 0 + +reactable(filled_eth_prices[1:10, c(1,3,4,5)] %>% round(.,2)) + +``` + +For these blocks, since no trades occurred, the prices of the pools have not changed, and thus the previous block price is still valid. + +```{r} +# Improves analysis speed to front-load these calculations and is more smoothed +filled_eth_prices$ETH_WAVG_PRICE <- zoo::na.locf(filled_eth_prices$ETH_WAVG_PRICE) + +reactable(filled_eth_prices[1:10, c(1,3,4,5)] %>% round(.,2)) +``` + +## ETH Market Price + +We can then look back 100 (99+1) blocks and take the median of the volume weighted average price. + +Post-merge, 100 blocks is exactly 1200 seconds, or 20 minutes. This smooths price volatility, +but is also reactive to persistent large changes in price. For the first 100 blocks, we simply retain the weighted average price already calculated since there is not enough blocks to lookback to. + +This `ETH MARKET PRICE` smooths short term volatility. For example, in blocks 15,000,500 - 15,000,510 +the market price says ~ 1,120.4 both before and after the weighted average price fluctuates from 1,119.62 to 1,121.12. + +```{r} +ETH_MARKET_PRICE <- zoo::rollmedian(x = filled_eth_prices$ETH_WAVG_PRICE, k = 99, align = "left") +diff_median <- nrow(filled_eth_prices) - length(ETH_MARKET_PRICE) +ETH_MARKET_PRICE <- c(filled_eth_prices$ETH_WAVG_PRICE[1:diff_median], ETH_MARKET_PRICE) + +filled_eth_prices$ETH_MARKET_PRICE <- ETH_MARKET_PRICE + +reactable(filled_eth_prices[501:510, c(1,3,4,5,6)] %>% round(., 2)) +``` + +# Confirmation of Key Benefits + +Reviewing the 3 motivations, the first is clear, we have a price *at the block level* which is significantly more granular than available 3rd party sources. The other motivations will be clarified mathematically. + +## Market Price is Realistic + +Singular market prices don't really exist. The price of ETH/USD on Coinbase versus ETH/USD on Binance +result from high frequency trading from market specific buyers and sellers. Arbitrage bots and traders make tiny bits of profit buying where the asset is cheaper and selling where the asset is more expensive. This keeps the prices on different exchanges very close together but not exactly equal. + +For our market price to be *realistic* we'd want to verify that it is possible to get this price in a short amount of time. If we claim the market price is 1,500 USD for 1 ETH; but all the markets are trading at 1,600 - 16,03; +our market price is obviously wrong. Whereas a market price of 1,602 USD for 1 ETH would be tolerable. In the *very* short term (i.e., within seconds or 1 minute) that price can be actualized on at least 1 exchange. + +Here, we'll measure our Market Price as realistic based on how many blocks it takes for the original volume weighted average price (VWAP: `ETH_WAVG_PRICE`) to *cross* our `ETH_MARKET_PRICE`. If the market price is above the VWAP, but the VWAP quickly flips to above the market price, we can say the market price was realistic because it was possible to trade at that price in that block range. + +The longer it takes for the VWAP to cross the market price, the worse we should consider our market price. + +We can measure these crosses by first identifying where VWAP > Market Price. + +```{r} +# TRUE OR FALSE +filled_eth_prices$WAVG_OVER_MARKET <- filled_eth_prices$ETH_WAVG_PRICE > filled_eth_prices$ETH_MARKET_PRICE +filled_eth_prices$ETH_WAVG_PRICE <- round(filled_eth_prices$ETH_WAVG_PRICE, 2) +filled_eth_prices$ETH_MARKET_PRICE <- round(filled_eth_prices$ETH_MARKET_PRICE, 2) +reactable(filled_eth_prices[501:510, c(1,3,6,7)]) + +``` + +Regardless of whether the VWAP is above or below the market price; what matters is the flip. +If the `WAVG_OVER_MARKET` == LAG(`WAVG_OVER_MARKET`) the market has not crossed. But when it flips +from true: VWAP > Market Price to false: VWAP <= Market Price; OR the reverse. Then we can argue the Market +Price was possible at that block. + +Again, the longer it takes to cross, the less realistic our market price. + +Here, At block 15,000,502 the VWAP went from 1,119.62 in the previous block to 1,120.67; +this crosses the 1,120.40 Market Price, i.e. this price was available between these blocks, making our +CROSS variable true. + +```{r} + +# First block FALSE b/c it doesn't have a lag +filled_eth_prices$CROSS <- c(FALSE, + filled_eth_prices$WAVG_OVER_MARKET[-1] != + filled_eth_prices$WAVG_OVER_MARKET[-nrow(filled_eth_prices)]) + +reactable(filled_eth_prices[501:510, c(1,3,6,7,8)]) + +``` + +A few summary stats for CROSS are provided: + +```{r} + +cross_diffs <- diff(which(filled_eth_prices$CROSS)) + +cd_tbl <- as.data.frame(table(cross_diffs)) +colnames(cd_tbl) <- c("block_diff", "count") +cd_tbl$cumulative_amount <- cumsum(cd_tbl$count) +cd_tbl$cumulative_percent <- cd_tbl$cumulative_amount/sum(cd_tbl$count) + +reactable( + data.frame( + "# Blocks" = nrow(filled_eth_prices), + "# Crosses" = sum(filled_eth_prices$CROSS), + "Cross %" = 100*sum(filled_eth_prices$CROSS)/nrow(filled_eth_prices), + "Avg Blocks btws Cross" = mean(cross_diffs), + "Median Blocks btwn Cross" = median(cross_diffs), + "Min Blocks btwn Cross" = min(cross_diffs), + "Max Blocks btwn Cross" = max(cross_diffs), + check.names = FALSE) %>% round(. , 2) +) + +plot_ly(data = cd_tbl, + x = ~block_diff, y = ~cumulative_percent, + type = "scatter", mode = "lines", + name = "Cumulative Percent") %>% + layout( + title = list(text = "90% of the time, Market Price can be realized in ~20 blocks", y = 0.975), + xaxis = list(title = "# of Blocks until Cross (ordered)"), + yaxis = list(title = "Cumulative Percentage") + ) +``` + +The key takeaway, is the Market Price is realistic, as it can be realized in 4 or less minutes over 90% of the time in our sample. The median time until the VWAP crosses is only 3 blocks (< 1 minute). + +Exceeding 100 blocks (20 minutes) occurred about 1.1% of the time, with the worst in our block sample being 326 blocks (65 minutes), which is about equivalent to a Chainlink update w/o the volatility trigger for early update. + + +### Clarification + +**Note:** This is **not** to say the market price methodology solves the price forecasting problem. Here, for a set of Market Prices [M~a~ to M~b~] the corresponding VWAP prices [V~a~ to V~b~] for Block A to Block B, is such that there is both a [M~i~ < V~i~] and [M~k~ > V~k~] where i and k are between Block A and Block B. + +In a flash crash situation, a single Market Price [M~i~] may be strictly above all [V~a~ to V~b~]. +Similarly in sudden rise situation, a single Market Price [M~i~] be be strictly below all [V~a~ to V~b~]. + +This would be true for any range where a local minimum or local maximum exist in that range, i.e., "timing the top or bottom". + +To measure Market Price's ability to forecast a reversion, let's identify the % of Market Price in the data where a rolling 100 & 1000 block forward look has both a lower minimum and higher maximum VWAP than the Market Price. + +This is a different, potentially unfair measure for any method to be realistic, but it's an important clarification to what Market Price cannot do. + +```{r} + +market_note <- filled_eth_prices %>% + mutate( + # No rollmin function lol; min is negative max negative though. + future_min100 = -1*c(zoo::rollmax(x = ETH_WAVG_PRICE*-1, k = 101, align = "right"),rep(NA,100)), + future_min1000 = -1*c(zoo::rollmax(x = ETH_WAVG_PRICE*-1, k = 1001, align = "right"),rep(NA,1000)), + future_max100 = c(zoo::rollmax(x = ETH_WAVG_PRICE, k = 101, align = "right"),rep(NA,100)), + future_max1000 = c(zoo::rollmax(x = ETH_WAVG_PRICE, k = 1001, align = "right"),rep(NA,1000)) + ) %>% + mutate( + revert100 = (ETH_MARKET_PRICE > future_min100 & ETH_MARKET_PRICE <= future_max100), + revert1000 = (ETH_MARKET_PRICE >= future_min1000 & ETH_MARKET_PRICE <= future_max1000) + ) + +reactable( + data.frame( + "Market 100 Block Revert %" = sum(market_note$revert100, na.rm = TRUE)/length(!is.na(market_note$revert100)), + "Market 1,000 Block Revert %" = sum(market_note$revert1000, na.rm = TRUE)/length(!is.na(market_note$revert1000)), + check.names = FALSE + ) %>% round(., 2) +) +``` + +Here, 75% of the time, Market Price is within the next 100 Blocks min and max VWAP. 93% of the time it is in the next 1,000 Blocks min and max VWAP. + +An (expectedly) lower measure of 'realistic'. + +## Market Price is Smooth + +Two simple measures are available to measure smoothness of Market Price vs Volume Weighted Average Price; first is simple variance. But this measure undersells the benefit. The standard deviation (sqrt of variance) for +VWAP is `r sd(filled_eth_prices$ETH_WAVG_PRICE)` while for Market Price it is `r sd(filled_eth_prices$ETH_MARKET_PRICE)`, an only marginal difference. + +It's more clear when comparing the partial autocorrelation of the two values. Autocorrelation is the +correlation between a series of numbers and its previous value (e.g., comparing today's weather to yesterday's). +Short term price changes can be affected by both recent price changes ("momentum" trading as related to some relevant news event) and randomness (coincidental trading not because of price but because of some external factors like needing cash to pay taxes). + +Partial autocorrelation attempts to control momentum by adjusting for more history (e.g., comparing today's weather to yesterday's while knowing it's been cold all week). + +Here, the absolute value of the partial autocorrelation (up to 60 lags) shows a stark contract. +Block level VWAP has >10% correlation going back up to 5 blocks. While Market Price never correlates with +its previous values that much. + +This indicates that it is resistant to irrelevant random swings that beget more swings in the same direction. + +```{r} +vwap_pacf <- pacf(filled_eth_prices$ETH_WAVG_PRICE, plot = FALSE)$acf %>% round(., 2) +market_pacf <- pacf(filled_eth_prices$ETH_MARKET_PRICE, plot = FALSE)$acf %>% round(., 2) + +pacf_compare <- data.frame( + lag = 2:length(vwap_pacf), + vwap = abs(vwap_pacf[-1]), + market = abs(market_pacf[-1]) +) + +plot_ly(data = pacf_compare, x = ~lag, y = ~vwap, + type = "scatter", mode = "lines", name = "VWAP") %>% +add_trace(x = ~lag, y = ~market, name = "Market") %>% + layout( + title = list(text = "Market Price never has >10% PAC, unlike VWAP w/ 5 Block Momentum", y = 0.975), + xaxis = list(title = "Partial Autocorrelation Lag (# Blocks preceding)"), + yaxis = list (title = "Correlation %") + ) +``` + +Combine this knowledge with the previous evidence of consistent Crosses between VWAP and Market Price and it's clear this Market Price methodology is both realistic and smooth. + +## Market Price is Reactive + +Lastly, there are real large price changes that are not purely random. Big news like confirmation of the next major Ethereum upgrades, or Macro level changes in the US Federal Funds Rate are all correlated with large, non-random, changes in ETH's price. + +A strong on-chain pricing methodology should be able to filter noise (which we saw previously in showing that it is smooth) while not ignoring signal (sustained large changes). + +Note: While this was partially confirmed by looking at how realistic the prices were (90% of crosses happened within 20 blocks) here is the absolute % difference over time. + +### Absolute % Diff over Time + +When VWAP price changes rapidly in a sustained way, % difference from the market price +should close quickly as well. Sustained differences between market price and VWAP is +a sign that the methodology is not recognizing important historical signal. + +```{r} + +filled_eth_prices <- filled_eth_prices %>% mutate( + percent_deviation = abs( (ETH_MARKET_PRICE-ETH_WAVG_PRICE)/ETH_WAVG_PRICE ) +) + +plot_ly(data = filled_eth_prices, + x = ~BLOCK_NUMBER, + y = ~percent_deviation, + type = "scatter", mode = "lines") %>% + layout( + title = list(text = "Spikes in % deviation fall nearly immediately", y = 0.975), + xaxis = list(title = "Block Number"), + yaxis = list (title = "Absolute % Diff (Market-VWAP)/VWAP") + ) + + +``` + +Volume Weighted Average Price has a known issue. Given a limited number of pools and +known Maximum Extractable Value (MEV), we've identified several spikes in VWAP that +are, simply put, *bad trades*. Occasionally, aggregator users will be routed across different DEXes and a percentage of their trade will be at high slippage or have disproportionate price-impact. + +For example, here, in block 15,288,205 the ETH-USDT pool becomes incredibly unbalanced from a large +multi-million dollar swap related to some AAVE withdrawals intersecting a poorly priced 1Inch Aggregator trade. + +The two key transaction hashes are provided here: + +- [MEV Bot Interaction](https://etherscan.io/tx/0x8e79a8b5480518e0ad65b9a725532cd2dde330499561e222e428a1d779ac5e90) where USDT <> ETH was at $2100+ + +- [1Inch/0x Aggregation](https://etherscan.io/tx/0x388d31432f523eca8a6958c8509b9430a31c773a046dc5cb177405d69a76259b) where USDT <> ETH was at $2800+. + +Whereas the market price was closer to ~$1,715. + +```{r} +reactable( + filled_eth_prices[filled_eth_prices$BLOCK_NUMBER %in% (15288200:15288209), c(1,3,4,6,9)] %>% round(., 4) +) + +``` + +Thus, while at first glance 5-10%+ price deviation between VWAP and Market Price can be alarming, +it is more often correctly smoothing chaotic on-chain outliers. + +```{r} + +pd_tbl <- as.data.frame(table(filled_eth_prices$percent_deviation %>% round(., 3))) +colnames(pd_tbl) <- c("abs_price_dev", "count") +pd_tbl$cumulative_amount <- cumsum(pd_tbl$count) +pd_tbl$cumulative_percent <- pd_tbl$cumulative_amount/sum(pd_tbl$count) + + +plot_ly(data = pd_tbl, + x = ~abs_price_dev, y = ~cumulative_percent, + type = "scatter", mode = "lines", + name = "Cumulative Percent") %>% + layout( + title = list(text = "98% of the time, Market Price is within 1% of VWAP", y = 0.975), + xaxis = list(title = "Absolute % Gap btween Market and VWAP"), + yaxis = list(title = "Cumulative Percentage") + ) + +``` + +# Not for Forecasting + +This methodology is *not* a financial forecast. For the same reason that the partial autocorrelation for Market Price was near 0, these prices are ultimately smoothed followers of price action. + +For a glaring example, watch VWAP collapse over only 600 Blocks on 2022-08-19. + +```{r} +plot_ly(data = filled_eth_prices, x=~BLOCK_NUMBER, + y = ~ETH_WAVG_PRICE, + type = "scatter", + mode = "lines", name = "VWAP") %>% + add_trace(x=~BLOCK_NUMBER, + y = ~ETH_MARKET_PRICE, name = "Market") %>% + layout( + title = list(text = "Market Price still *Lags* on Flash Crashes", y = 0.975), + xaxis = list(title = "Block Number", range = c(15369600, 15370200)), + yaxis = list(title = "Price", range = c(1750, 1820)) + ) + +plot_ly(data = filled_eth_prices, x=~BLOCK_NUMBER, + y = ~( (ETH_WAVG_PRICE - ETH_MARKET_PRICE)/ETH_WAVG_PRICE ), + hovertext = ~BLOCK_TIMESTAMP, + type = "scatter", + mode = "lines", name = "VWAP") %>% + layout( + title = list(text = "Real Crashes can happen fast!", y = 0.975), + xaxis = list(title = "Block Number", range = c(15369600, 15370200)), + yaxis = list(title = "% VWAP > Market", range = c(-.05, .05)) + ) + +``` + +# Conclusion + +When you want to go back in time and assign a price to a token, high volatility can skew your analysis, especially short-term volatility. Volume Weighted Average Price at the block level is useful but can be skewed by bad trades. The methodology shown here, rolling 99-block median VWAP, is realistic, smooth, and (with a lag) reactive to structural changes in price. + +Consider using these methods for retroactive pricing in your future analysis. And again, FlipsideCrypto's Research is open source. Check out all the code for this report [here](https://github.com/FlipsideCrypto/onchain-pricing) on github. + diff --git a/renv.lock b/renv.lock index 675c7c1..8f2dd47 100644 --- a/renv.lock +++ b/renv.lock @@ -9,6 +9,24 @@ ] }, "Packages": { + "MASS": { + "Package": "MASS", + "Version": "7.3-57", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "71476c1d88d1ebdf31580e5a257d5d31", + "Requirements": [] + }, + "Matrix": { + "Package": "Matrix", + "Version": "1.4-1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "699c47c606293bdfbc9fd78a93c9c8fe", + "Requirements": [ + "lattice" + ] + }, "R6": { "Package": "R6", "Version": "2.5.1", @@ -17,6 +35,22 @@ "Hash": "470851b6d5d0ac559e9d01bb352b4021", "Requirements": [] }, + "RColorBrewer": { + "Package": "RColorBrewer", + "Version": "1.1-3", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "45f0398006e83a5b10b72a90663d8d8c", + "Requirements": [] + }, + "Rcpp": { + "Package": "Rcpp", + "Version": "1.0.10", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "e749cae40fa9ef469b6050959517453c", + "Requirements": [] + }, "askpass": { "Package": "askpass", "Version": "1.1", @@ -72,6 +106,35 @@ "Hash": "3177a5a16c243adc199ba33117bd9657", "Requirements": [] }, + "colorspace": { + "Package": "colorspace", + "Version": "2.1-0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "f20c47fd52fae58b4e377c37bb8c335b", + "Requirements": [] + }, + "cpp11": { + "Package": "cpp11", + "Version": "0.4.3", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "ed588261931ee3be2c700d22e94a29ab", + "Requirements": [] + }, + "crosstalk": { + "Package": "crosstalk", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "6aa54f69598c32177e920eb3402e8293", + "Requirements": [ + "R6", + "htmltools", + "jsonlite", + "lazyeval" + ] + }, "curl": { "Package": "curl", "Version": "5.0.0", @@ -80,6 +143,14 @@ "Hash": "e4f97056611e8e6b8b852d13b7400cf1", "Requirements": [] }, + "data.table": { + "Package": "data.table", + "Version": "1.14.6", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "aecef50008ea7b57c76f1cb5c127fb02", + "Requirements": [] + }, "digest": { "Package": "digest", "Version": "0.6.31", @@ -88,6 +159,25 @@ "Hash": "8b708f296afd9ae69f450f9640be8990", "Requirements": [] }, + "dplyr": { + "Package": "dplyr", + "Version": "1.0.10", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "539412282059f7f0c07295723d23f987", + "Requirements": [ + "R6", + "generics", + "glue", + "lifecycle", + "magrittr", + "pillar", + "rlang", + "tibble", + "tidyselect", + "vctrs" + ] + }, "ellipsis": { "Package": "ellipsis", "Version": "0.3.2", @@ -106,6 +196,22 @@ "Hash": "4b68aa51edd89a0e044a66e75ae3cc6c", "Requirements": [] }, + "fansi": { + "Package": "fansi", + "Version": "1.0.4", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "1d9e7ad3c8312a192dea7d3db0274fde", + "Requirements": [] + }, + "farver": { + "Package": "farver", + "Version": "2.1.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "8106d78941f34855c440ddb946b8f7a5", + "Requirements": [] + }, "fastmap": { "Package": "fastmap", "Version": "1.1.0", @@ -122,6 +228,35 @@ "Hash": "7c89603d81793f0d5486d91ab1fc6f1d", "Requirements": [] }, + "generics": { + "Package": "generics", + "Version": "0.1.3", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "15e9634c0fcd294799e9b2e929ed1b86", + "Requirements": [] + }, + "ggplot2": { + "Package": "ggplot2", + "Version": "3.4.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "fd2aab12f54400c6bca43687231e246b", + "Requirements": [ + "MASS", + "cli", + "glue", + "gtable", + "isoband", + "lifecycle", + "mgcv", + "rlang", + "scales", + "tibble", + "vctrs", + "withr" + ] + }, "glue": { "Package": "glue", "Version": "1.6.2", @@ -130,6 +265,14 @@ "Hash": "4f2596dfb05dac67b9dc558e5c6fba2e", "Requirements": [] }, + "gtable": { + "Package": "gtable", + "Version": "0.3.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "36b4265fb818f6a342bed217549cd896", + "Requirements": [] + }, "highr": { "Package": "highr", "Version": "0.10", @@ -154,6 +297,20 @@ "rlang" ] }, + "htmlwidgets": { + "Package": "htmlwidgets", + "Version": "1.6.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "b677ee5954471eaa974c0d099a343a1a", + "Requirements": [ + "htmltools", + "jsonlite", + "knitr", + "rmarkdown", + "yaml" + ] + }, "httr": { "Package": "httr", "Version": "1.4.4", @@ -168,6 +325,14 @@ "openssl" ] }, + "isoband": { + "Package": "isoband", + "Version": "0.2.7", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "0080607b4a1a7b28979aecef976d8bc2", + "Requirements": [] + }, "jquerylib": { "Package": "jquerylib", "Version": "0.1.4", @@ -200,6 +365,41 @@ "yaml" ] }, + "labeling": { + "Package": "labeling", + "Version": "0.4.2", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "3d5108641f47470611a32d0bdf357a72", + "Requirements": [] + }, + "later": { + "Package": "later", + "Version": "1.3.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "7e7b457d7766bc47f2a5f21cc2984f8e", + "Requirements": [ + "Rcpp", + "rlang" + ] + }, + "lattice": { + "Package": "lattice", + "Version": "0.20-45", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "b64cdbb2b340437c4ee047a1f4c4377b", + "Requirements": [] + }, + "lazyeval": { + "Package": "lazyeval", + "Version": "0.2.2", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "d908914ae53b04d4c0c0fd72ecc35370", + "Requirements": [] + }, "lifecycle": { "Package": "lifecycle", "Version": "1.0.3", @@ -231,6 +431,17 @@ "rlang" ] }, + "mgcv": { + "Package": "mgcv", + "Version": "1.8-40", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "c6b2fdb18cf68ab613bd564363e1ba0d", + "Requirements": [ + "Matrix", + "nlme" + ] + }, "mime": { "Package": "mime", "Version": "0.12", @@ -239,6 +450,26 @@ "Hash": "18e9c28c1d3ca1560ce30658b22ce104", "Requirements": [] }, + "munsell": { + "Package": "munsell", + "Version": "0.5.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "6dfe8bf774944bd5595785e3229d8771", + "Requirements": [ + "colorspace" + ] + }, + "nlme": { + "Package": "nlme", + "Version": "3.1-157", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "dbca60742be0c9eddc5205e5c7ca1f44", + "Requirements": [ + "lattice" + ] + }, "openssl": { "Package": "openssl", "Version": "2.0.5", @@ -249,6 +480,96 @@ "askpass" ] }, + "packrat": { + "Package": "packrat", + "Version": "0.9.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "2735eb4b51c302014f53acbb3c80a65f", + "Requirements": [] + }, + "pillar": { + "Package": "pillar", + "Version": "1.8.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "f2316df30902c81729ae9de95ad5a608", + "Requirements": [ + "cli", + "fansi", + "glue", + "lifecycle", + "rlang", + "utf8", + "vctrs" + ] + }, + "pkgconfig": { + "Package": "pkgconfig", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "01f28d4278f15c76cddbea05899c5d6f", + "Requirements": [] + }, + "plotly": { + "Package": "plotly", + "Version": "4.10.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "3781cf6971c6467fa842a63725bbee9e", + "Requirements": [ + "RColorBrewer", + "base64enc", + "crosstalk", + "data.table", + "digest", + "dplyr", + "ggplot2", + "htmltools", + "htmlwidgets", + "httr", + "jsonlite", + "lazyeval", + "magrittr", + "promises", + "purrr", + "rlang", + "scales", + "tibble", + "tidyr", + "vctrs", + "viridisLite" + ] + }, + "promises": { + "Package": "promises", + "Version": "1.2.0.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "4ab2c43adb4d4699cf3690acd378d75d", + "Requirements": [ + "R6", + "Rcpp", + "later", + "magrittr", + "rlang" + ] + }, + "purrr": { + "Package": "purrr", + "Version": "1.0.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "d71c815267c640f17ddbf7f16144b4bb", + "Requirements": [ + "cli", + "lifecycle", + "magrittr", + "rlang", + "vctrs" + ] + }, "rappdirs": { "Package": "rappdirs", "Version": "0.3.3", @@ -257,6 +578,30 @@ "Hash": "5e3c5dc0b071b21fa128676560dbe94d", "Requirements": [] }, + "reactR": { + "Package": "reactR", + "Version": "0.4.4", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "75389c8091eb14ee21c6bc87a88b3809", + "Requirements": [ + "htmltools" + ] + }, + "reactable": { + "Package": "reactable", + "Version": "0.4.3", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "4fec6f287e77503ceb822812722930d0", + "Requirements": [ + "digest", + "htmltools", + "htmlwidgets", + "jsonlite", + "reactR" + ] + }, "renv": { "Package": "renv", "Version": "0.15.5", @@ -292,6 +637,30 @@ "yaml" ] }, + "rsconnect": { + "Package": "rsconnect", + "Version": "0.8.29", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "fe178fc15af80952f546aafedf655b36", + "Requirements": [ + "curl", + "digest", + "jsonlite", + "openssl", + "packrat", + "rstudioapi", + "yaml" + ] + }, + "rstudioapi": { + "Package": "rstudioapi", + "Version": "0.14", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "690bd2acc42a9166ce34845884459320", + "Requirements": [] + }, "sass": { "Package": "sass", "Version": "0.4.4", @@ -306,6 +675,23 @@ "rlang" ] }, + "scales": { + "Package": "scales", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "906cb23d2f1c5680b8ce439b44c6fa63", + "Requirements": [ + "R6", + "RColorBrewer", + "farver", + "labeling", + "lifecycle", + "munsell", + "rlang", + "viridisLite" + ] + }, "shroomDK": { "Package": "shroomDK", "Version": "0.1.1", @@ -349,6 +735,57 @@ "Hash": "34c16f1ef796057bfa06d3f4ff818a5d", "Requirements": [] }, + "tibble": { + "Package": "tibble", + "Version": "3.1.8", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "56b6934ef0f8c68225949a8672fe1a8f", + "Requirements": [ + "fansi", + "lifecycle", + "magrittr", + "pillar", + "pkgconfig", + "rlang", + "vctrs" + ] + }, + "tidyr": { + "Package": "tidyr", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "cdb403db0de33ccd1b6f53b83736efa8", + "Requirements": [ + "cpp11", + "dplyr", + "ellipsis", + "glue", + "lifecycle", + "magrittr", + "purrr", + "rlang", + "tibble", + "tidyselect", + "vctrs" + ] + }, + "tidyselect": { + "Package": "tidyselect", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "79540e5fcd9e0435af547d885f184fd5", + "Requirements": [ + "cli", + "glue", + "lifecycle", + "rlang", + "vctrs", + "withr" + ] + }, "tinytex": { "Package": "tinytex", "Version": "0.43", @@ -359,6 +796,14 @@ "xfun" ] }, + "utf8": { + "Package": "utf8", + "Version": "1.2.2", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "c9c462b759a5cc844ae25b5942654d13", + "Requirements": [] + }, "vctrs": { "Package": "vctrs", "Version": "0.5.1", @@ -372,6 +817,22 @@ "rlang" ] }, + "viridisLite": { + "Package": "viridisLite", + "Version": "0.4.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "62f4b5da3e08d8e5bcba6cac15603f70", + "Requirements": [] + }, + "withr": { + "Package": "withr", + "Version": "2.5.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "c0e49a9760983e81e55cdd9be92e7182", + "Requirements": [] + }, "xfun": { "Package": "xfun", "Version": "0.36", @@ -387,6 +848,16 @@ "Repository": "CRAN", "Hash": "9b570515751dcbae610f29885e025b41", "Requirements": [] + }, + "zoo": { + "Package": "zoo", + "Version": "1.8-11", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "874a5b77fe0cfacf2a3450069ae70926", + "Requirements": [ + "lattice" + ] } } }