From 523f99e0c69d2e86adbfd4e9bb763dc1b05ea81a Mon Sep 17 00:00:00 2001 From: Vikas Negi <68782261+vnegi10@users.noreply.github.com> Date: Sat, 25 Jun 2022 23:03:08 +0200 Subject: [PATCH] Update format (#17) * Add JuliaFormatter config file * Rename + split files * Format getdata* functions * Format plots* functions * Format helper.jl * Format app.jl * Format runtests.jl * Reduce line cols in docs * Update minor version * Make var. naming consistent * Update README * Add header comment --- .JuliaFormatter.toml | 17 + Project.toml | 4 +- README.md | 23 +- docs/src/index.md | 78 ++- src/ConfigureApp.jl | 385 --------------- src/CryptoDashApp.jl | 13 +- src/CryptoFunctions.jl | 136 ------ src/PlotFunctions.jl | 335 ------------- src/app.jl | 557 ++++++++++++++++++++++ src/getdataAV.jl | 90 ++++ src/{GetDataFunctions.jl => getdataCG.jl} | 242 ++++------ src/helper.jl | 158 ++++++ src/plotsAV.jl | 391 +++++++++++++++ src/plotsCG.jl | 74 +++ test/runtests.jl | 52 +- 15 files changed, 1485 insertions(+), 1070 deletions(-) create mode 100644 .JuliaFormatter.toml delete mode 100644 src/ConfigureApp.jl delete mode 100644 src/CryptoFunctions.jl delete mode 100644 src/PlotFunctions.jl create mode 100644 src/app.jl create mode 100644 src/getdataAV.jl rename src/{GetDataFunctions.jl => getdataCG.jl} (54%) create mode 100644 src/helper.jl create mode 100644 src/plotsAV.jl create mode 100644 src/plotsCG.jl diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 0000000..a7a2b8d --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1,17 @@ +# Configuration file for JuliaFormatter.jl +# Formatting options are described here: https://github.com/domluna/JuliaFormatter.jl + +indent = 4 +margin = 80 +remove_extra_newlines = true +trailing_comma = false + +always_for_in = true +always_use_return = true + +whitespace_typedefs = true +whitespace_ops_in_indices = true +whitespace_in_kwargs = true + +verbose = true +overwrite = true \ No newline at end of file diff --git a/Project.toml b/Project.toml index 4fc27ef..055f342 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "CryptoDashApp" uuid = "8f55a569-a1ee-45d2-b761-b2b316ddd8e9" authors = ["Vikas Negi "] -version = "0.1.5" +version = "0.2.0" [deps] AlphaVantage = "6348297c-a006-11e8-3a05-9bbf8830fd7b" @@ -33,4 +33,4 @@ JSON = "0.21" PlotlyJS = "0.15" Query = "1.0" StatsBase = "0.33" -julia = "1.6" +julia = "1.6" \ No newline at end of file diff --git a/README.md b/README.md index ba23ca9..3ff20ac 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,27 @@ [![](https://img.shields.io/badge/docs-stable-blue.svg)](https://vnegi10.github.io/CryptoDashApp.jl/stable) -Interactive visualization of historical price (currently only in EUR), health metrics, candlestick and volume data for various cryptocurrencies. Data are obtained via API queries to Alpha Vantage. -The app is written in Julia and makes use of Dash.jl along with its react framework to generate a dashboard style view accessible via a browser. -Different moving averages are also calculated and plotted along with the daily average price data. Averaging window can be selected by the user. +Interactive visualization of historical price (currently only in EUR), health metrics, candlestick +and volume data for various cryptocurrencies. Data are obtained via API queries to Alpha Vantage. +The app is written in Julia and makes use of Dash.jl along with its react framework to generate a +dashboard style view accessible via a browser. Different moving averages are also calculated and +plotted along with the daily average price data. Averaging window can be selected by the user. -Functionality of the app will be extended in the future by adding more technical indicators. +Functionality of the app will be extended in the future by adding more technical indicators. +**Contributions are most welcome!** ## How to use? -This package is available via the General registry. Add it to your working Julia environment by doing the following in the REPL: +This package is available via the General registry. Add it to your working Julia environment by +doing the following in the REPL: * Press ']' to enter Pkg prompt ```julia pkg> add CryptoDashApp ``` -Once all the packages are downloaded, you can make use of the **CryptoDashApp** module from the REPL itself or within a script by executing: +Once all the packages are downloaded, you can make use of the **CryptoDashApp** module from the +REPL itself or within a script by executing: ```julia julia> using CryptoDashApp @@ -30,7 +35,11 @@ Free API key can be obtained from [here.](https://www.alphavantage.co/support/#a Now, open a new tab in your browser and check if you can access http://0.0.0.0:8056/ or http://127.0.0.1:8056/ -First plot will take a few seconds to appear, wait patiently! If you are running this app for the first time, or on a different date, new data will be retrieved from Alpha Vantage, and then saved to CSV files on disk. Keep in mind that the free API key imposes a limit of five calls/minute, which means you won't be able to load data for more than five currencies in quick succession. In case you see an error, wait for a while and then try again. +First plot will take a few seconds to appear, wait patiently! If you are running this app for the +first time, or on a different date, new data will be retrieved from Alpha Vantage, and then saved +to CSV files on disk. Keep in mind that the free API key imposes a limit of five calls/minute, +which means you won't be able to load data for more than five currencies in quick succession. In +case you see an error, wait for a while and then try again. ## App demo diff --git a/docs/src/index.md b/docs/src/index.md index d7c387a..a3405ea 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -2,21 +2,37 @@ --- ## Overview -CryptoDashApp.jl creates a dashboard that allows the user to monitor historical cryptocurrency market data and calculate some key technical indicators. Data is obtained from Alpha Vantage for which an API key is necessary. Free API keys can be created [here.](https://www.alphavantage.co/support/#api-key) +CryptoDashApp.jl creates a dashboard that allows the user to monitor historical +cryptocurrency market data and calculate some key technical indicators. Data is +obtained from Alpha Vantage for which an API key is necessary. Free API keys can +be created [here.](https://www.alphavantage.co/support/#api-key) -Do note that the number of API calls are restricted to five per minute and 500 requests per day (more than enough for our app). We somewhat circumvent the former limitation by saving the data to the disk in the form of CSV files. Before downloading fresh data for a given date, older files are deleted. +Do note that the number of API calls are restricted to five per minute and 500 +requests per day (more than enough for our app). We somewhat circumvent the former +limitation by saving the data to the disk in the form of CSV files. Before downloading +fresh data for a given date, older files are deleted. ## App UI -Data type and currency of interest can be selected via drop-down lists. Currently, data for only one currency can be viewed at a time. Averaging window (for calculating various moving averages) and historical timeframe can be selected via checkboxes. +Data type and currency of interest can be selected via drop-down lists. Currently, data +for only one currency can be viewed at a time. Averaging window (for calculating various +moving averages) and historical timeframe can be selected via checkboxes. -The plots are interactive, which means you can hover your cursor over the data points to see their value, and also zoom into a particular region. There is also an option to download the plot as a png file. By clicking on the legends, you can turn off the display for that data, which is useful for example when you want to look at only a certain type of data. +The plots are interactive, which means you can hover your cursor over the data points +to see their value, and also zoom into a particular region. There is also an option to +download the plot as a png file. By clicking on the legends, you can turn off the display +for that data, which is useful for example when you want to look at only a certain type +of data. ## Available modes ### Average price + Daily trade -- Left plot shows the daily average price for a given currency. Averaging is done by taking the mean of open, close, high and low price for all days. When a moving average window other than 1-day is selected, the SMA (Simple Moving Average), WMA (Weighted Moving Average) and EMA (Exponential Moving Average) are also visible. +- Left plot shows the daily average price for a given currency. Averaging is done by taking +the mean of open, close, high and low price for all days. When a moving average window other +than 1-day is selected, the SMA (Simple Moving Average), WMA (Weighted Moving Average) and +EMA (Exponential Moving Average) are also visible. -- Right plot shows the daily trade data, which is simply the product of volume (number of coins) multiplied by the average price. +- Right plot shows the daily trade data, which is simply the product of volume (number of +coins) multiplied by the average price. ### Candlestick + Volume - Left plot shows the candlestick data for selected timeframe. @@ -26,22 +42,40 @@ The plots are interactive, which means you can hover your cursor over the data p ### Cumulative + Daily return - Left plot shows the cumulative return that will be obtained from a given starting date. -- Right plot shows the daily return distribution. Green indicates an increase in price, whereas red indicates the opposite. Percentage share of green/red is also indicated in the legend. +- Right plot shows the daily return distribution. Green indicates an increase in price, +whereas red indicates the opposite. Percentage share of green/red is also indicated in +the legend. ### Daily volatility -- This plot shows the distribution of the daily price change for a given currency over the specified timeframe. A wider distribution (higher 3σ) indicates a more volatile behavior. +- This plot shows the distribution of the daily price change for a given currency over +the specified timeframe. A wider distribution (higher 3σ) indicates a more volatile behavior. ### MACD + Signal -- Left plot shows the daily average price data along with 12-day and 26-day EMAs. These are used to calculate the Moving Average Convergence Divergence (MACD) indicator, which is shown on the right. +- Left plot shows the daily average price data along with 12-day and 26-day EMAs. These are +used to calculate the Moving Average Convergence Divergence (MACD) indicator, which is shown +on the right. -- Right plot shows the MACD (26-day EMA subtracted from 12-day EMA) along with its signal line (9-day EMA of the MACD). A buy signal occurs when MACD crosses the signal line from below, and a sell signal occurs when the crossing is from above. Distance between the MACD and the signal line is shown via bars in the same plot, with green bars (bullish momentum) indicating the region where MACD > signal, and red bars (bearish momentum) for the opposite. For more description, see [link.](https://www.investopedia.com/terms/m/macd.asp) +- Right plot shows the MACD (26-day EMA subtracted from 12-day EMA) along with its signal +line (9-day EMA of the MACD). A buy signal occurs when MACD crosses the signal line from below, +and a sell signal occurs when the crossing is from above. Distance between the MACD and the signal +line is shown via bars in the same plot, with green bars (bullish momentum) indicating the region +where MACD > signal, and red bars (bearish momentum) for the opposite. For more description, +see [link.](https://www.investopedia.com/terms/m/macd.asp) ### Linear regression channel -- This metric can be used to identify trends and their direction. Plot shows a linear regression line that is the best fit to the data available for the selected duration. The **upper channel** line runs parallel to the linear regression line, and is located two standard deviations above it. Same goes for the **lower channel** line except that it's located two standard deviations below. +- This metric can be used to identify trends and their direction. Plot shows a linear regression +line that is the best fit to the data available for the selected duration. The **upper channel** line +runs parallel to the linear regression line, and is located two standard deviations above it. +Same goes for the **lower channel** line except that it's located two standard deviations below. -- When the price drops below the lower channel, it is considered as a buy signal. Similarly, price rising above the upper channel leads to a sell signal. If the price spends time outside the channel, it could indicate that a trend reversal is imminent. +- When the price drops below the lower channel, it is considered as a buy signal. Similarly, price +rising above the upper channel leads to a sell signal. If the price spends time outside the channel, +it could indicate that a trend reversal is imminent. -- Plot title also contains $R^2$, which is a statistical measure representing the proportion of variation in the dependent variable that is explained by different features (independent variables) in this model. A value closer to 1 (maximum) indicates a good fit. The linear regression channel metric will therefore not be very useful when $R^2 < 0.50$. +- Plot title also contains $R^2$, which is a statistical measure representing the proportion of +variation in the dependent variable that is explained by different features (independent variables) +in this model. A value closer to 1 (maximum) indicates a good fit. The linear regression channel metric +will therefore not be very useful when $R^2 < 0.50$. ### Bollinger bands - These are price envelopes plotted at two standard deviations (std) above (upper band) and below @@ -58,23 +92,31 @@ the upper band might be considered as the next profit target. Prices can also cr during strong trends. ### Fundamental Cryptocurrency Asset Score (FCAS) data -- This metric tells us about the market health of an asset. In the case of cryptocurrencies, they are user activity, developer behavior, and market maturity, which are provided by [Flipside Crypto](https://app.flipsidecrypto.com/tracker/all-coins). +- This metric tells us about the market health of an asset. In the case of cryptocurrencies, they are +user activity, developer behavior, and market maturity, which are provided +by [Flipside Crypto](https://app.flipsidecrypto.com/tracker/all-coins). ### Following modes make use of the CoinGecko API --- ### Developer + Community data -- Left plot shows various developer metrics (number of stars in the repository, commits, closed issues, pull requests etc.) that gives us an idea on how active the software development team is for a given cryptocurrency. In particular, for an actively maintained repository, the delta between the number of "closed issues" and the number of "total issues" should not be too big. +- Left plot shows various developer metrics (number of stars in the repository, commits, closed issues, +pull requests etc.) that gives us an idea on how active the software development team is for a given +cryptocurrency. In particular, for an actively maintained repository, the delta between the number of +"closed issues" and the number of "total issues" should not be too big. -- Right plot shows social media activity (number of followers, accounts etc.) on popular platforms such as reddit, telegram and twitter. +- Right plot shows social media activity (number of followers, accounts etc.) on popular platforms +such as reddit, telegram and twitter. ### Exchange volume data per currency -- Left plot shows the total volume (number of coins) that has been traded over last 24 hours for the selected cryptocurrency on the top ten exchanges. +- Left plot shows the total volume (number of coins) that has been traded over last 24 hours for the +selected cryptocurrency on the top ten exchanges. - Right plot shows the USD equivalent for the same data. ### Overall exchange volume data -- This stacked bar plot shows the total 24-hr volume (in BTC) of all the cryptocurrencies traded on various exchanges over the course of last 30 days. +- This stacked bar plot shows the total 24-hr volume (in BTC) of all the cryptocurrencies traded on +various exchanges over the course of last 30 days. ## Run app ```@docs diff --git a/src/ConfigureApp.jl b/src/ConfigureApp.jl deleted file mode 100644 index 7ca4f6b..0000000 --- a/src/ConfigureApp.jl +++ /dev/null @@ -1,385 +0,0 @@ -################# Parameters to interact with the web app ################# - -currencies_list = ["BTC", "LTC", "BCH", "ETH", "KNC", "LINK", "ETC", "BNB", "ADA", "XTZ", - "EOS", "XRP", "XLM", "ZEC", "DASH", "XMR", "DOT", "UNI", "SOL", "MATIC", - "THETA", "OMG", "ALGO", "GRT", "AAVE", "FIL", "BAT", "ZRX", "COMP"] - -currencies = sort(currencies_list) -currencies_index = 1:length(currencies) - -modes = ["Average price + Daily trade (AV)", - "Candlestick + Volume (AV)", - "Cumulative + Daily return (AV)", - "Daily volatility (AV)", - "MACD + Signal (AV)", - "Linear regression channel (AV)", - "Bollinger bands (AV)", - "FCAS data (AV)", - "Developer + Community data (CG)", - "Exchange volume data per currency (CG)", - "Overall exchange volume data (CG)"] - -modes_index = 1:length(modes) - -durations = [7, 14, 30, 90, 180, 270, 365, 500, 750, 1000] -windows = [1, 5, 10, 30, 50, 75, 100] - - -################# CoinGecko URL ################# - -const URL = "https://api.coingecko.com/api/v3" - - -################# Cleanup function ################# - -function remove_old_files() - # Cleanup data files from previous days - try - main_dir = pwd() - cd("data") - files = readdir() - rx1 = "data" - rx2 = "List" - rx3 = ".csv" - rx4 = ".txt" - for file in files - ts = Dates.unix2datetime(stat(file).mtime) - file_date = Date(ts) - if file_date != Dates.today() && (occursin(rx3, file) || occursin(rx4, file)) && - (occursin(rx1, file) || occursin(rx2, file)) - rm(file) - end - end - cd(main_dir) - catch - @info "Unable to perform cleanup action" - end -end - - -################# Run the app ################# - -""" - run_app(port::Int64, key::String) - -Start the app on the given port using the provided API key. - -# Arguments -- `port::Int64` : Port number, for example 8056 -- `key::String` : API key from Alpha Vantage - -# Example -```julia-repl -julia> run_app(8056, "Your API key") -[ Info: data folder exists, cleanup action will be performed! -[ Info: Listening on: 0.0.0.0:8056 -[ Info: Fetching BTC price/vol data from Alpha Vantage -``` -""" -function run_app(port::Int64, key::String) - - # Check if "data" folder exists, if not, create a new one - if isdir("data") - @info "data folder exists, cleanup action will be performed!" - else - mkdir("data") - @info "New data folder has been created" - end - - # Perform cleanup - remove_old_files() - - # Set API key - AlphaVantage.global_key!(key) - - # UI part of the tool - app = dash() - - app.layout = html_div() do - - # Title - html_h1("Crypto Dashboard", - style= ( - textAlign = "center", - ) - ), - - # Subtitle - html_div(style=(width="75%", margin="auto", textAlign="center"), - - ["Visualization of market data obtained from", - html_a(" Alpha Vantage", href="https://www.alphavantage.co/documentation/"), - - html_a(" and"), - - html_a(" CoinGecko", href="https://www.coingecko.com/en/api/documentation"), - - html_a(" until $(Dates.today())"), - - html_p(" "), - ]), - - # First row of drop down buttons - html_div(className="row", [ - html_div(className="col-8", - html_table(style=(width="100%", textAlign="center"), - vcat(html_tr([html_th("Type of data", style=(width="50%", textAlign="center")), - html_th("Cryptocurrency", style=(width="75%", textAlign="center"))]), - [html_tr([ - html_td(dcc_dropdown( - id = "mode_ID", - options = [ - (label = "$(modes[i])", value = i) for i in modes_index - ], - value = 1, - )), - - html_td(dcc_dropdown( - id = "pair_ID", - options = [ - (label = "$(currencies[i])", value = i) for i in currencies_index - ], - value = 1, - ))])] - )))]), - - # Add space between the two rows - html_div(style=(width="75%", margin="auto", textAlign="center"), - [html_p(" "), ]), - - # Second row of radio items - html_div(className="row", [ - html_div(className="col-8", - html_table(style=(width="100%", textAlign="center"), - vcat(html_tr([html_th("Moving average window", style=(width="50%", textAlign="center")), - html_th("Historical data [days]", style=(width="75%", textAlign="center"))]), - [html_tr([ - html_td(dcc_radioitems( - id = "window_ID", - options = [(label = "$(i)-day", value = i) for i in windows], - value = 1, - )), - - html_td(dcc_radioitems( - id = "duration_ID", - options = [(label = "$(i)d", value = i) for i in durations], - value = 7, - ))])] - )))]), - - # Plots - html_div(style = (width="100%", display="inline-block", margin="0 5% 0 2%")) do - dcc_graph(id = "graph") - end - end - - callback!( - app, - Output("graph", "figure"), - Input("mode_ID", "value"), - Input("pair_ID", "value"), - Input("window_ID", "value"), - Input("duration_ID", "value"), - ) do mode_ID, pair_ID, window_ID, duration_ID - - if mode_ID == 1 - t1, t2, t3, t4, t5 = plot_price_ma_trade_data(pair_ID, duration_ID, window_ID) - - layout1 = Layout(;title="Daily average price data for $(currencies[pair_ID])", - xaxis = attr(title="Time", showgrid=true, zeroline=true, linewidth=1.0), - yaxis = attr(title="Price [euros]", showgrid=true, zeroline=true, linewidth=1.0), - height = 500, - width = 1000, - paper_bgcolor="white" - ) - layout2 = Layout(;title="Daily trade data (volume x price) for $(currencies[pair_ID])", - xaxis = attr(title="Time", showgrid=true, zeroline=true, linewidth=1.0), - yaxis = attr(title="Daily trade [euros]", showgrid=true, zeroline=true, linewidth=1.0), - height = 500, - width = 1000, - paper_bgcolor="white" - ) - P1 = Plot([t1, t2, t3, t4], layout1) # plots daily average price and three diferent moving averages - P2 = Plot(t5, layout2) # plots daily market cap - return [P1 P2] - - elseif mode_ID == 2 - t1, t2 = plot_candle_vol_data(pair_ID, duration_ID) - - layout1 = Layout(;title="Candlestick data for $(currencies[pair_ID])", - xaxis=attr(title="Time", showgrid=true, zeroline=true), - yaxis=attr(title="Price [euros]", zeroline=true), - height = 500, - width = 1000, - ) - layout2 = Layout(;title="Daily volume data for $(currencies[pair_ID])", - xaxis=attr(title="Time", showgrid=true, zeroline=true), - yaxis=attr(title="Volume [Number of coins]", zeroline=true), - height = 100, - width = 200 - ) - P1 = Plot(t1, layout1) # plots candlestick data - P2 = Plot(t2, layout2) # plots daily volume - return [P1 P2] - - elseif mode_ID == 3 - t1, t2_green, t2_red, _ , _ , _ = plot_cumul_daily_return_hist(pair_ID, duration_ID) - - layout1 = Layout(;title="Cumulative return for $(currencies[pair_ID])", - xaxis=attr(title="Time", showgrid=true, zeroline=true), - yaxis=attr(title="Return [in %]", zeroline=true), - height = 500, - width = 1000, - ) - layout2 = Layout(;title="Daily return for $(currencies[pair_ID])", - xaxis=attr(title="Time", showgrid=true, zeroline=true), - yaxis=attr(title="Return [in %]", zeroline=true), - height = 500, - width = 1000, - barmode = "group", - ) - - P1 = Plot(t1, layout1) # plots cumulative return % - P2 = Plot([t2_green, t2_red], layout2) # plots daily change % - return [P1 P2] - - elseif mode_ID == 4 - _ , _ , _ , t1 , σ, duration = plot_cumul_daily_return_hist(pair_ID, duration_ID) - - layout1 = Layout(;title="Distribution of daily price change for $(currencies[pair_ID]) over $(duration) days, 3σ = $(round(3*σ, digits = 2)) %", - xaxis=attr(title="Change [in %]", showgrid=true, zeroline=true), - yaxis=attr(title="Number of counts", zeroline=true), - height = 500, - width = 1000, - ) - - P1 = Plot(t1, layout1) # plots histogram of daily price change - return [P1] - - elseif mode_ID == 5 - t1, t2, t3, t4, t5, t6_green, t6_red = plot_macd_signal(pair_ID, duration_ID) - - layout1 = Layout(;title="EMA and average price data for $(currencies[pair_ID])", - xaxis = attr(title="Time", showgrid=true, zeroline=true, linewidth=1.0), - yaxis = attr(title="Price [euros]", showgrid=true, zeroline=true, linewidth=1.0), - height = 500, - width = 1000, - ) - layout2 = Layout(;title="MACD and signal for $(currencies[pair_ID])", - xaxis = attr(title="Time", showgrid=true, zeroline=true, linewidth=1.0), - yaxis = attr(title="Indicator [euros]", showgrid=true, zeroline=true, linewidth=1.0), - height = 500, - width = 1000, - ) - - P1 = Plot([t1, t2, t3], layout1) # plots average price, EMA-12 and EMA-26 - P2 = Plot([t4, t5, t6_green, t6_red], layout2) # plots MACD, signal and their distance - return [P1 P2] - - elseif mode_ID == 6 - t1, t2, t3, t4, R² = plot_linear_regression(pair_ID, duration_ID) - - layout1 = Layout(;title="Linear regression channel for $(currencies[pair_ID]) price over last $(duration_ID) days, R² = $(R²)", - xaxis = attr(title="Time", showgrid=true, zeroline=true), - yaxis = attr(title="Price [euros]", showgrid=true, zeroline=true), - height = 500, - width = 1000, - ) - - P1 = Plot([t1, t2, t3, t4], layout1) # plots linear regression channel - return [P1] - - elseif mode_ID == 7 - t1, t2, t3, t4 = plot_price_bollinger_bands(pair_ID, duration_ID, window_ID) - - layout1 = Layout(;title="Bollinger bands for $(currencies[pair_ID])", - xaxis = attr(title="Time", showgrid=true, zeroline=true), - yaxis = attr(title="Price [euros]", showgrid=true, zeroline=true), - height = 500, - width = 1000, - ) - - P1 = Plot([t1, t2, t3, t4], layout1) # plots Bollinger bands - return [P1] - - elseif mode_ID == 8 - t1, fr = plot_fcas_data(pair_ID) - - layout1 = Layout(;title="FCAS metrics data for $(currencies[pair_ID]), overall rating = $(fr)", - xaxis = attr(title="Type of metric", showgrid=true, zeroline=true), - yaxis = attr(title="Score", showgrid=true, zeroline=true), - height = 500, - width = 1000, - ) - - P1 = Plot(t1, layout1) # plots FCAS metrics - return [P1] - - # From CoinGecko - elseif mode_ID == 9 - t1, t2 = plot_dev_comm_data(pair_ID) - - layout1 = Layout(;title="Developer metrics for $(currencies[pair_ID])", - xaxis = attr(title="", showgrid=true, zeroline=true, automargin=true), - xaxis_tickangle = -22.5, - yaxis = attr(title="Value", showgrid=true, zeroline=true), - height = 500, - width = 1000, - ) - layout2 = Layout(;title="Community metrics for $(currencies[pair_ID])", - xaxis = attr(title="", showgrid=true, zeroline=true, automargin=true), - xaxis_tickangle = -22.5, - yaxis = attr(title="Value", showgrid=true, zeroline=true), - height = 500, - width = 1000, - ) - - P1 = Plot(t1, layout1) # plots developer data - P2 = Plot(t2, layout2) # plots community data - return [P1 P2] - - elseif mode_ID == 10 - - t1, t2 = plot_exchange_vol_data(pair_ID) - - layout1 = Layout(;title="Exchange volume data (24h) for $(currencies[pair_ID])", - xaxis = attr(title="", showgrid=true, zeroline=true, automargin=true), - xaxis_tickangle = -22.5, - yaxis = attr(title="Number of coins", showgrid=true, zeroline=true), - height = 500, - width = 1000, - ) - layout2 = Layout(;title="Exchange volume data (24h) for $(currencies[pair_ID])", - xaxis = attr(title="", showgrid=true, zeroline=true, automargin=true), - xaxis_tickangle = -22.5, - yaxis = attr(title="Volume in USD", showgrid=true, zeroline=true), - height = 500, - width = 1000, - ) - - P1 = Plot(t1, layout1) # plots coin volume data for a given currency - P2 = Plot(t2, layout2) # plots USD volume data for a given currency - return [P1 P2] - - elseif mode_ID == 11 - - t_all = plot_overall_vol_data(duration_ID) - - layout1 = Layout(;title="Overall historical volume data (24h) for top 10 exchanges", - xaxis = attr(title="", showgrid=true, zeroline=true, automargin=true), - xaxis_tickangle = 0.0, - yaxis = attr(title="Volume in BTC", showgrid=true, zeroline=true), - height = 500, - width = 1000, - barmode = "stack", - ) - - P1 = Plot(t_all, layout1) # plots overall BTC volume data for all exchanges - return P1 - - end - end - - # Allows access from a web browser, port can be changed to any valid entry - run_server(app, "0.0.0.0", port) -end \ No newline at end of file diff --git a/src/CryptoDashApp.jl b/src/CryptoDashApp.jl index a79b99f..427538c 100644 --- a/src/CryptoDashApp.jl +++ b/src/CryptoDashApp.jl @@ -6,9 +6,14 @@ using AlphaVantage, DataFrames, Dates, PlotlyJS, Dash, DashHtmlComponents, DashCoreComponents, CSV, Statistics, StatsBase, GLM, HTTP, JSON, Query, DelimitedFiles -include("CryptoFunctions.jl") -include("GetDataFunctions.jl") -include("PlotFunctions.jl") -include("ConfigureApp.jl") +include("app.jl") + +include("getdataAV.jl") +include("getdataCG.jl") + +include("plotsAV.jl") +include("plotsCG.jl") + +include("helper.jl") end # module \ No newline at end of file diff --git a/src/CryptoFunctions.jl b/src/CryptoFunctions.jl deleted file mode 100644 index 499c43a..0000000 --- a/src/CryptoFunctions.jl +++ /dev/null @@ -1,136 +0,0 @@ -function raw_to_df(raw_data) - - # Argument ":auto" is required to generate column names in latest version of DataFrames.jl (v1.2.1) - df = DataFrame(raw_data[1], :auto) - df_names = Symbol.(vcat(raw_data[2]...)) - df = DataFrames.rename(df, df_names) - - timestamps = df[!,:timestamp] - - select!(df, Not([:timestamp])) - - for col in eachcol(df) - col = Float64.(col) - end - - df[!,:Date] = Date.(timestamps) - return df -end - -function average_price_df(currency::String, df_in::DataFrame) - - df_out_price, df_out_candle = [DataFrame() for i = 1:2] - - df_out_price[!,:Date] = df_in[!,:Date] - - df_out_price[!,Symbol("$currency")] = (df_in[!,Symbol("open (EUR)")] + df_in[!,Symbol("high (EUR)")] + df_in[!,Symbol("low (EUR)")] + df_in[!,Symbol("close (EUR)")])/4 - - candle_col = Any[] - for i in 1:size(df_in)[1] - push!(candle_col, (df_in[!,Symbol("open (EUR)")][i], df_in[!,Symbol("high (EUR)")][i], df_in[!,Symbol("low (EUR)")][i], df_in[!,Symbol("close (EUR)")][i])) - end - - df_out_candle[!,:Date] = df_in[!,:Date] - - df_out_candle[!,Symbol("$currency")] = candle_col - - return df_out_price, df_out_candle -end - -function vol_df(currency::String, df_in::DataFrame) - - df_out_vol = DataFrame() - - df_out_vol[!,:Date] = df_in[!,:Date] - df_out_vol[!,Symbol("$currency")] = df_in[!,:volume] - - return df_out_vol -end - -function moving_averages(Price_df::DataFrame, duration::Int64, window::Int64) - - # Price_df should have date order - oldest to latest - Price_col = Price_df[end-duration+1-window+1:end,2] - rows1 = length(Price_col) - Price_SMA, Price_WMA, Price_EMA = [Float64[] for i = 1:3] - - weights = collect(1:window)/(window*(window+1)/2) - k = 2/(window+1) - - # Calculate different Moving Averages - for i = 1:rows1-(window-1) - # Simple Moving Average (SMA) - push!(Price_SMA, mean(Price_col[i:i+(window-1)])) - - # Weighted Moving Average - push!(Price_WMA, sum(Price_col[i:i+(window-1)].*weights)) - - # Exponential Moving Average - SMA = mean(Price_col[i:i+(window-1)]) - EMA = (Price_col[i+(window-1)]*k)+(SMA*(1-k)) - push!(Price_EMA, EMA) - end - - return Price_SMA, Price_WMA, Price_EMA -end - -function moving_std(Price_df::DataFrame, duration::Int64, window::Int64) - - # Price_df should have date order - oldest to latest - Price_col = Price_df[end-duration+1-window+1:end,2] - rows1 = length(Price_col) - - Price_std = Float64[] - - for i = 1:rows1-(window-1) - # Standard deviation over the period SMA is also being calculated - push!(Price_std, std(Price_col[i:i+(window-1)])) - end - - return Price_std -end - -function calculate_ema(Price_col::Vector{Float64}, window::Int64) - - k(window) = 2/(window+1) - Price_EMA = Float64[] - rows = length(Price_col) - - for i = 1:rows-(window-1) - - SMA = mean(Price_col[i:i+(window-1)]) - EMA = (Price_col[i+(window-1)]*k(window)) + - (SMA*(1-k(window))) - push!(Price_EMA, EMA) - end - - return Price_EMA -end - -function calculate_macd(Price_df::DataFrame, window_long::Int64 = 26, - window_short::Int64 = 12, window_signal::Int64 = 9) - - # Price_df should have date order - oldest to latest - Price_col = Price_df[!, 2] - - # Calculate 26 (long) and 12 (short) period EMA - Price_EMA_short = calculate_ema(Price_col, window_short) - Price_EMA_long = calculate_ema(Price_col, window_long) - - # Make EMA_long and EMA_short equal - EMA_short_col = Price_EMA_short[window_long - window_short + 1:end] - - # Calculate MACD = EMA_short - EMA_long - MACD_col = EMA_short_col - Price_EMA_long - - # Calculate signal line (9 period EMA of MACD) - Signal_col = calculate_ema(MACD_col, window_signal) - - df_ema = DataFrame(date = Price_df[window_long+window_signal-1:end, :Date], - Raw = Price_df[window_long+window_signal-1:end, 2], - EMA_long = Price_EMA_long[window_signal:end], - EMA_short = EMA_short_col[window_signal:end], - MACD = MACD_col[window_signal:end], Signal = Signal_col) - - return df_ema -end \ No newline at end of file diff --git a/src/PlotFunctions.jl b/src/PlotFunctions.jl deleted file mode 100644 index 69bc84d..0000000 --- a/src/PlotFunctions.jl +++ /dev/null @@ -1,335 +0,0 @@ -function plot_price_ma_trade_data(index::Int64, duration::Int64, window::Int64) - # Retrieve data from various helper functions - Price_df, _ , Vol_df = get_price_data_single(currencies[index]) - - # Make sure that duration does not exceed the number of rows - max(windows) in the DataFrame - # This allows calculation of MA for the longest duration - if duration > size(Price_df)[1]-maximum(windows) - duration = size(Price_df)[1]-maximum(windows) - end - - ################# Daily average data ################# - trace1 = PlotlyJS.scatter(;x = Price_df[1:duration,:Date], y = Price_df[1:duration,2], mode="markers+lines", name = "$(currencies[index]) price") - - ################# Moving averages data ################# - sort!(Price_df, :Date) # oldest date first, newest at the bottom - Price_SMA, Price_WMA, Price_EMA = moving_averages(Price_df, duration, window) - - trace2 = PlotlyJS.scatter(;x = Price_df[!,:Date][end-length(Price_SMA)+1:end], y = Price_SMA, mode="lines", name = "$(names(Price_df)[2]) SMA over $(window) days") - trace3 = PlotlyJS.scatter(;x = Price_df[!,:Date][end-length(Price_WMA)+1:end], y = Price_WMA, mode="lines", name = "$(names(Price_df)[2]) WMA over $(window) days") - trace4 = PlotlyJS.scatter(;x = Price_df[!,:Date][end-length(Price_EMA)+1:end], y = Price_EMA, mode="lines", name = "$(names(Price_df)[2]) EMA over $(window) days") - - ################# Daily trade data ################# - sort!(Price_df, :Date, rev = true) # newest date first, oldest at the bottom - trace5 = PlotlyJS.bar(;x = Price_df[1:duration,:Date], y = Price_df[1:duration,2].*Vol_df[1:duration,2] , mode="markers+lines", name = "$(currencies[index]) daily trade") - - return trace1, trace2, trace3, trace4, trace5 -end - -function plot_price_bollinger_bands(index::Int64, duration::Int64, window::Int64) - - Price_df, _ , _ = get_price_data_single(currencies[index]) - - if duration > size(Price_df)[1]-maximum(windows) - duration = size(Price_df)[1]-maximum(windows) - end - - sort!(Price_df, :Date) # oldest date first, newest at the bottom - Price_SMA, _ , _ = moving_averages(Price_df, duration, window) - Price_σ = moving_std(Price_df, duration, window) - - ################# Raw price data ################# - - trace1 = PlotlyJS.scatter(;x = Price_df[!,:Date][end-length(Price_SMA)+1:end], - y = Price_df[!,2][end-length(Price_SMA)+1:end], - mode = "markers+lines", - name = "$(currencies[index]) price") - - ################# SMA and Bollinger bands ################# - - trace2 = PlotlyJS.scatter(;x = Price_df[!,:Date][end-length(Price_SMA)+1:end], - y = Price_SMA, - mode = "lines", - name = "$(names(Price_df)[2]) SMA over $(window) days") - - trace3 = PlotlyJS.scatter(;x = Price_df[!,:Date][end-length(Price_SMA)+1:end], - y = Price_SMA .+ 2*Price_σ, - mode = "markers", - name = "Upper band (+2σ)") - - trace4 = PlotlyJS.scatter(;x = Price_df[!,:Date][end-length(Price_SMA)+1:end], - y = Price_SMA .- 2*Price_σ, - mode = "markers", - name = "Lower band (-2σ)") - - return trace1, trace2, trace3, trace4 -end - -function plot_candle_vol_data(index::Int64, duration::Int64) - - # Retrieve data from various helper functions - _ , Candle_df, Vol_df = get_price_data_single(currencies[index]) - - # Make sure that duration does not exceed the number of rows in the DataFrame - if duration > size(Candle_df)[1] - duration = size(Candle_df)[1] - end - - ################# Daily candlestick data ################# - open_col, high_col, low_col, close_col = [Float64[] for i = 1:4] - - for i = 1:duration - push!(open_col, Candle_df[!,2][i][1]) - push!(high_col, Candle_df[!,2][i][2]) - push!(low_col, Candle_df[!,2][i][3]) - push!(close_col, Candle_df[!,2][i][4]) - end - - trace1 = PlotlyJS.candlestick(; x = Candle_df[1:duration,:Date], open = open_col, high = high_col, low = low_col, - close = close_col, name = "$(currencies[index])") - - - ################# Daily volume data ################# - trace2 = PlotlyJS.bar(;x = Vol_df[1:duration,:Date], y = Vol_df[1:duration,2], name = "$(currencies[index]) volume") - - return trace1, trace2 -end - -function plot_cumul_daily_return_hist(index::Int64, duration::Int64) - - # Retrieve data from various helper functions - Price_df , _ , _ = get_price_data_single(currencies[index]) - - # Make sure that duration does not exceed the number of rows in the DataFrame - if duration > size(Price_df)[1] - duration = size(Price_df)[1] - end - - # Reverse the order (oldest date first, newest at the bottom) - sort!(Price_df, :Date) - - ################# Cumulative return ################# - - trace1 = PlotlyJS.scatter(; x = Price_df[end-duration+2:end,:Date], y = (cumsum(diff(Price_df[end-duration+1:end,2])) ./ Price_df[end-duration+1, 2]) .* 100, - mode="markers+lines", name = "$(currencies[index]) cumulative return") - - ################# Daily return ################# - - X = Price_df[end-duration+2:end,:Date] - Y = (diff(Price_df[end-duration+1:end,2]) ./ Price_df[end-duration+1:end-1,2]) .* 100 - - # Split into two datasets (green: positive change, red: negative change) - green_Y = Y[Y .≥ 0.0] - green_X = X[Y .≥ 0.0] - - red_Y = Y[Y .< 0.0] - red_X = X[Y .< 0.0] - - green_share = round((length(green_Y)/length(Y))*100, digits = 2) - red_share = 100.0 - green_share - - trace2_green = PlotlyJS.bar(; x = green_X, y = green_Y, marker_color = "green", name = "$(currencies[index]) increase, share = $(green_share) %") - trace2_red = PlotlyJS.bar(; x = red_X, y = red_Y, marker_color = "red", name = "$(currencies[index]) decrease, share = $(red_share) %") - - ################# Daily change histogram / Daily volatility ################# - - σ = round(std(Y), digits = 2) - trace3 = plotly_hist(Y, 75; normalize = false) - - return trace1, trace2_green, trace2_red, trace3, σ, duration -end - -function plotly_hist(x::AbstractVector{T}, nbins::Integer; normalize::Bool = true) where T <: Number - - # use StatsBase to create a histogram object - hist = StatsBase.fit(Histogram, x, nbins = nbins, closed = :left) - - # obtain bar positions -> center of each interval - bins = similar(x, length(hist.edges[1]) - 1) - edges = hist.edges[1] - - for k in eachindex(bins) - bins[k] = (edges[k] + edges[k + 1]) * 0.5 - end - - if normalize - y = hist.weights ./ length(x) # we need a new array - else - y = hist.weights - end - - trace = bar(x = bins, y = y) - - return trace -end - -function plot_fcas_data(index::Int64) - - us, fs, ds, ms, fr = get_ratings_data(currencies[index]) - - ################# FCAS metrics data ################# - trace1 = PlotlyJS.bar(;x = ["Utility", "FCAS", "Developer", "Market maturity"], y = [us, fs, ds, ms], width = 0.25) - return trace1, fr -end - -function plot_macd_signal(index::Int64, duration::Int64) - - # Retrieve data from various helper functions - Price_df, _ , _ = get_price_data_single(currencies[index]) - - # Make sure that duration does not exceed the number of rows in the DataFrame - if duration > size(Price_df)[1] - duration = size(Price_df)[1] - end - - sort!(Price_df, :Date) # oldest date first, newest at the bottom - - Price_df = Price_df[end-duration+1-26-9+1:end, :] # filter based on selected duration and effective - # window size of 26+9 - - df_ema_all = calculate_macd(Price_df) # get EMA and MACD data into a DataFrame - - - ################# Daily average, EMA-12 and EMA-26 data ################# - trace1 = PlotlyJS.scatter(;x = df_ema_all[!,:date], y = df_ema_all[!,:Raw], - mode="markers+lines", name = "Daily average") - - trace2 = PlotlyJS.scatter(;x = df_ema_all[!,:date], y = df_ema_all[!,:EMA_long], - mode="markers+lines", name = "EMA-26") - - trace3 = PlotlyJS.scatter(;x = df_ema_all[!,:date], y = df_ema_all[!,:EMA_short], - mode="markers+lines", name = "EMA-12") - - - ################# MACD and its signal data ################# - trace4 = PlotlyJS.scatter(;x = df_ema_all[!,:date], y = df_ema_all[!,:MACD], - mode="markers+lines", name = "MACD") - - trace5 = PlotlyJS.scatter(;x = df_ema_all[!,:date], y = df_ema_all[!,:Signal], - mode="markers+lines", name = "Signal (EMA-9)") - - - ################# Distance between MACD and its signal line ################# - - X = df_ema_all[!,:date] - Y = df_ema_all[!,:MACD] - df_ema_all[!,:Signal] - - # Split into two datasets (green: positive change, red: negative change) - green_Y = Y[Y .≥ 0.0] - green_X = X[Y .≥ 0.0] - - red_Y = Y[Y .< 0.0] - red_X = X[Y .< 0.0] - - trace6_green = PlotlyJS.bar(; x = green_X, y = green_Y, marker_color = "green", - name = "MACD above signal ") - - trace6_red = PlotlyJS.bar(; x = red_X, y = red_Y, marker_color = "red", - name = "MACD below signal") - - return trace1, trace2, trace3, trace4, trace5, trace6_green, trace6_red - -end - -function plot_linear_regression(index::Int64, duration::Int64) - - # Retrieve data from various helper functions - Price_df, _ , _ = get_price_data_single(currencies[index]) - - # Make sure that duration does not exceed the number of rows in the DataFrame - if duration > size(Price_df)[1] - duration = size(Price_df)[1] - end - - # Filter on duration and create index column (model fit does not work with dates) - df_fit = sort!(Price_df[1:duration, :]) - df_fit.Index = 1:size(df_fit)[1] - - # Rename column to price to make df_fit generic - rename!(df_fit, Dict(Symbol(names(df_fit)[2]) => "Price")) - - # Get model parameters and R² - model_params = lm(@formula(Price ~ Index), df_fit) - R² = round(r2(model_params), digits = 2) - - # Predicted data - price_from_model = Float64.(predict(model_params, df_fit)) - - # Get standard deviation σ - σ = std(price_from_model) - - ################# Raw price data ################# - - trace1 = PlotlyJS.scatter(;x = df_fit[!,:Date], y = df_fit[!,:Price], - mode="markers+lines", name = "Actual price") - - ################# Linear regression channel ################# - - trace2 = PlotlyJS.scatter(;x = df_fit[!,:Date], y = price_from_model, - mode="lines", name = "Linear regression line") - - trace3 = PlotlyJS.scatter(;x = df_fit[!,:Date], y = price_from_model .+ 2*σ, - mode="markers", name = "Upper channel (+2σ)") - - trace4 = PlotlyJS.scatter(;x = df_fit[!,:Date], y = price_from_model .- 2*σ, - mode="markers", name = "Lower channel (-2σ)") - - return trace1, trace2, trace3, trace4, R² -end - -function plot_dev_comm_data(index::Int64) - - # Convert currency symbol to lowercase and fetch data from CoinGecko - df_dev, df_comm = get_dev_comm_data(lowercase(currencies[index])) - - ################# Developer data ################# - trace1 = PlotlyJS.bar(; x = df_dev[!, :Metric], y = df_dev[!, :Value], - name = "Developer data") - - ################# Community data ################# - trace2 = PlotlyJS.bar(; x = df_comm[!, :Metric], y = df_comm[!, :Value], - name = "Community data") - - return trace1, trace2 -end - -function plot_exchange_vol_data(index::Int64, num_exchanges::Int64 = 10) - - # Convert currency symbol to lowercase and fetch data from CoinGecko - df_ex_vol = get_exchange_vol_data(lowercase(currencies[index]), num_exchanges) - - ################# Coin volume data ################# - trace1 = PlotlyJS.bar(; x = df_ex_vol[!, :Name], y = df_ex_vol[!, :Coin_volume], - name = "Volume data in coins") - - ################# USD volume data ################# - trace2 = PlotlyJS.bar(; x = df_ex_vol[!, :Name], y = df_ex_vol[!, :USD_volume], - name = "Volume data in USD") - - return trace1, trace2 -end - -function plot_overall_vol_data(duration::Int64, num_exchanges::Int64 = 10) - - # Collect all traces for all exchanges - all_traces = GenericTrace{Dict{Symbol, Any}}[] - - # Fetch overall volume data from CoinGecko for given historical duration - df_ex_vol = get_overall_vol_data(duration, num_exchanges) - - if ~isempty(df_ex_vol) - - # First column is for duration - exchanges = names(df_ex_vol)[2:end] - - for i = 1:length(exchanges) - - trace = PlotlyJS.bar(;x = df_ex_vol[!,:Time], y = df_ex_vol[!,i+1], - mode="markers+lines", name = "$(exchanges[i])") - - push!(all_traces, trace) - end - end - - return all_traces -end \ No newline at end of file diff --git a/src/app.jl b/src/app.jl new file mode 100644 index 0000000..20ac307 --- /dev/null +++ b/src/app.jl @@ -0,0 +1,557 @@ +################# Parameters to interact with the web app ################# + +currencies_list = [ + "BTC", + "LTC", + "BCH", + "ETH", + "KNC", + "LINK", + "ETC", + "BNB", + "ADA", + "XTZ", + "EOS", + "XRP", + "XLM", + "ZEC", + "DASH", + "XMR", + "DOT", + "UNI", + "SOL", + "MATIC", + "THETA", + "OMG", + "ALGO", + "GRT", + "AAVE", + "FIL", + "BAT", + "ZRX", + "COMP", +] + +currencies = sort(currencies_list) +currencies_index = 1:length(currencies) + +modes = [ + "Average price + Daily trade (AV)", + "Candlestick + Volume (AV)", + "Cumulative + Daily return (AV)", + "Daily volatility (AV)", + "MACD + Signal (AV)", + "Linear regression channel (AV)", + "Bollinger bands (AV)", + "FCAS data (AV)", + "Developer + Community data (CG)", + "Exchange volume data per currency (CG)", + "Overall exchange volume data (CG)", +] + +modes_index = 1:length(modes) + +durations = [7, 14, 30, 90, 180, 270, 365, 500, 750, 1000] +windows = [1, 5, 10, 30, 50, 75, 100] + + +################# CoinGecko URL ################# + +const URL = "https://api.coingecko.com/api/v3" + + +################# Cleanup function ################# + +function remove_old_files() + # Cleanup data files from previous days + try + main_dir = pwd() + cd("data") + files = readdir() + rx1 = "data" + rx2 = "List" + rx3 = ".csv" + rx4 = ".txt" + for file in files + ts = Dates.unix2datetime(stat(file).mtime) + file_date = Date(ts) + if file_date != Dates.today() && + (occursin(rx3, file) || occursin(rx4, file)) && + (occursin(rx1, file) || occursin(rx2, file)) + rm(file) + end + end + cd(main_dir) + catch + @info "Unable to perform cleanup action" + end +end + + +################# Run the app ################# + +""" + run_app(port::Int64, key::String) + +Start the app on the given port using the provided API key. + +# Arguments +- `port::Int64` : Port number, for example 8056 +- `key::String` : API key from Alpha Vantage + +# Example +```julia-repl +julia> run_app(8056, "Your API key") +[ Info: data folder exists, cleanup action will be performed! +[ Info: Listening on: 0.0.0.0:8056 +[ Info: Fetching BTC price/vol data from Alpha Vantage +``` +""" +function run_app(port::Int64, key::String) + + # Check if "data" folder exists, if not, create a new one + if isdir("data") + @info "data folder exists, cleanup action will be performed!" + else + mkdir("data") + @info "New data folder has been created" + end + + # Perform cleanup + remove_old_files() + + # Set API key + AlphaVantage.global_key!(key) + + # UI part of the tool + app = dash() + + app.layout = html_div() do + + # Title + html_h1("Crypto Dashboard", style = (textAlign = "center",)), + + # Subtitle + html_div( + style = (width = "75%", margin = "auto", textAlign = "center"), + [ + "Visualization of market data obtained from", + html_a( + " Alpha Vantage", + href = "https://www.alphavantage.co/documentation/", + ), + html_a(" and"), + html_a( + " CoinGecko", + href = "https://www.coingecko.com/en/api/documentation", + ), + html_a(" until $(Dates.today())"), + html_p(" "), + ], + ), + + # First row of drop down buttons + html_div( + className = "row", + [ + html_div( + className = "col-8", + html_table( + style = (width = "100%", textAlign = "center"), + vcat( + html_tr([ + html_th( + "Type of data", + style = (width = "50%", textAlign = "center"), + ), + html_th( + "Cryptocurrency", + style = (width = "75%", textAlign = "center"), + ), + ]), + [ + html_tr([ + html_td( + dcc_dropdown( + id = "mode_ID", + options = [ + (label = "$(modes[i])", value = i) for + i in modes_index + ], + value = 1, + ), + ), + html_td( + dcc_dropdown( + id = "pair_ID", + options = [ + (label = "$(currencies[i])", value = i) + for i in currencies_index + ], + value = 1, + ), + ), + ]), + ], + ), + ), + ), + ], + ), + + # Add space between the two rows + html_div( + style = (width = "75%", margin = "auto", textAlign = "center"), + [html_p(" ")], + ), + + # Second row of radio items + html_div( + className = "row", + [ + html_div( + className = "col-8", + html_table( + style = (width = "100%", textAlign = "center"), + vcat( + html_tr([ + html_th( + "Moving average window", + style = (width = "50%", textAlign = "center"), + ), + html_th( + "Historical data [days]", + style = (width = "75%", textAlign = "center"), + ), + ]), + [ + html_tr([ + html_td( + dcc_radioitems( + id = "window_ID", + options = [ + (label = "$(i)-day", value = i) for + i in windows + ], + value = 1, + ), + ), + html_td( + dcc_radioitems( + id = "duration_ID", + options = [ + (label = "$(i)d", value = i) for + i in durations + ], + value = 7, + ), + ), + ]), + ], + ), + ), + ), + ], + ), + + # Plots + html_div( + style = (width = "100%", display = "inline-block", margin = "0 5% 0 2%"), + ) do + dcc_graph(id = "graph") + end + end + + callback!( + app, + Output("graph", "figure"), + Input("mode_ID", "value"), + Input("pair_ID", "value"), + Input("window_ID", "value"), + Input("duration_ID", "value"), + ) do mode_ID, pair_ID, window_ID, duration_ID + + if mode_ID == 1 + t1, t2, t3, t4, t5 = plot_price_ma_trade_data(pair_ID, duration_ID, window_ID) + + layout1 = Layout(; + title = "Daily average price data for $(currencies[pair_ID])", + xaxis = attr( + title = "Time", + showgrid = true, + zeroline = true, + linewidth = 1.0, + ), + yaxis = attr( + title = "Price [euros]", + showgrid = true, + zeroline = true, + linewidth = 1.0, + ), + height = 500, + width = 1000, + paper_bgcolor = "white", + ) + layout2 = Layout(; + title = "Daily trade data (volume x price) for $(currencies[pair_ID])", + xaxis = attr( + title = "Time", + showgrid = true, + zeroline = true, + linewidth = 1.0, + ), + yaxis = attr( + title = "Daily trade [euros]", + showgrid = true, + zeroline = true, + linewidth = 1.0, + ), + height = 500, + width = 1000, + paper_bgcolor = "white", + ) + P1 = Plot([t1, t2, t3, t4], layout1) # plots daily average price and three diferent moving averages + P2 = Plot(t5, layout2) # plots daily market cap + return [P1 P2] + + elseif mode_ID == 2 + t1, t2 = plot_candle_vol_data(pair_ID, duration_ID) + + layout1 = Layout(; + title = "Candlestick data for $(currencies[pair_ID])", + xaxis = attr(title = "Time", showgrid = true, zeroline = true), + yaxis = attr(title = "Price [euros]", zeroline = true), + height = 500, + width = 1000, + ) + layout2 = Layout(; + title = "Daily volume data for $(currencies[pair_ID])", + xaxis = attr(title = "Time", showgrid = true, zeroline = true), + yaxis = attr(title = "Volume [Number of coins]", zeroline = true), + height = 100, + width = 200, + ) + P1 = Plot(t1, layout1) # plots candlestick data + P2 = Plot(t2, layout2) # plots daily volume + return [P1 P2] + + elseif mode_ID == 3 + t1, t2_green, t2_red, _, _, _ = + plot_cumul_daily_return_hist(pair_ID, duration_ID) + + layout1 = Layout(; + title = "Cumulative return for $(currencies[pair_ID])", + xaxis = attr(title = "Time", showgrid = true, zeroline = true), + yaxis = attr(title = "Return [in %]", zeroline = true), + height = 500, + width = 1000, + ) + layout2 = Layout(; + title = "Daily return for $(currencies[pair_ID])", + xaxis = attr(title = "Time", showgrid = true, zeroline = true), + yaxis = attr(title = "Return [in %]", zeroline = true), + height = 500, + width = 1000, + barmode = "group", + ) + + P1 = Plot(t1, layout1) # plots cumulative return % + P2 = Plot([t2_green, t2_red], layout2) # plots daily change % + return [P1 P2] + + elseif mode_ID == 4 + _, _, _, t1, σ, duration = plot_cumul_daily_return_hist(pair_ID, duration_ID) + + layout1 = Layout(; + title = "Distribution of daily price change for $(currencies[pair_ID]) over $(duration) days, 3σ = $(round(3*σ, digits = 2)) %", + xaxis = attr(title = "Change [in %]", showgrid = true, zeroline = true), + yaxis = attr(title = "Number of counts", zeroline = true), + height = 500, + width = 1000, + ) + + P1 = Plot(t1, layout1) # plots histogram of daily price change + return [P1] + + elseif mode_ID == 5 + t1, t2, t3, t4, t5, t6_green, t6_red = plot_macd_signal(pair_ID, duration_ID) + + layout1 = Layout(; + title = "EMA and average price data for $(currencies[pair_ID])", + xaxis = attr( + title = "Time", + showgrid = true, + zeroline = true, + linewidth = 1.0, + ), + yaxis = attr( + title = "Price [euros]", + showgrid = true, + zeroline = true, + linewidth = 1.0, + ), + height = 500, + width = 1000, + ) + layout2 = Layout(; + title = "MACD and signal for $(currencies[pair_ID])", + xaxis = attr( + title = "Time", + showgrid = true, + zeroline = true, + linewidth = 1.0, + ), + yaxis = attr( + title = "Indicator [euros]", + showgrid = true, + zeroline = true, + linewidth = 1.0, + ), + height = 500, + width = 1000, + ) + + P1 = Plot([t1, t2, t3], layout1) # plots average price, EMA-12 and EMA-26 + P2 = Plot([t4, t5, t6_green, t6_red], layout2) # plots MACD, signal and their distance + return [P1 P2] + + elseif mode_ID == 6 + t1, t2, t3, t4, R² = plot_linear_regression(pair_ID, duration_ID) + + layout1 = Layout(; + title = "Linear regression channel for $(currencies[pair_ID]) price over last $(duration_ID) days, R² = $(R²)", + xaxis = attr(title = "Time", showgrid = true, zeroline = true), + yaxis = attr(title = "Price [euros]", showgrid = true, zeroline = true), + height = 500, + width = 1000, + ) + + P1 = Plot([t1, t2, t3, t4], layout1) # plots linear regression channel + return [P1] + + elseif mode_ID == 7 + t1, t2, t3, t4 = plot_price_bollinger_bands(pair_ID, duration_ID, window_ID) + + layout1 = Layout(; + title = "Bollinger bands for $(currencies[pair_ID])", + xaxis = attr(title = "Time", showgrid = true, zeroline = true), + yaxis = attr(title = "Price [euros]", showgrid = true, zeroline = true), + height = 500, + width = 1000, + ) + + P1 = Plot([t1, t2, t3, t4], layout1) # plots Bollinger bands + return [P1] + + elseif mode_ID == 8 + t1, fr = plot_fcas_data(pair_ID) + + layout1 = Layout(; + title = "FCAS metrics data for $(currencies[pair_ID]), overall rating = $(fr)", + xaxis = attr(title = "Type of metric", showgrid = true, zeroline = true), + yaxis = attr(title = "Score", showgrid = true, zeroline = true), + height = 500, + width = 1000, + ) + + P1 = Plot(t1, layout1) # plots FCAS metrics + return [P1] + + # From CoinGecko + elseif mode_ID == 9 + t1, t2 = plot_dev_comm_data(pair_ID) + + layout1 = Layout(; + title = "Developer metrics for $(currencies[pair_ID])", + xaxis = attr( + title = "", + showgrid = true, + zeroline = true, + automargin = true, + ), + xaxis_tickangle = -22.5, + yaxis = attr(title = "Value", showgrid = true, zeroline = true), + height = 500, + width = 1000, + ) + layout2 = Layout(; + title = "Community metrics for $(currencies[pair_ID])", + xaxis = attr( + title = "", + showgrid = true, + zeroline = true, + automargin = true, + ), + xaxis_tickangle = -22.5, + yaxis = attr(title = "Value", showgrid = true, zeroline = true), + height = 500, + width = 1000, + ) + + P1 = Plot(t1, layout1) # plots developer data + P2 = Plot(t2, layout2) # plots community data + return [P1 P2] + + elseif mode_ID == 10 + + t1, t2 = plot_exchange_vol_data(pair_ID) + + layout1 = Layout(; + title = "Exchange volume data (24h) for $(currencies[pair_ID])", + xaxis = attr( + title = "", + showgrid = true, + zeroline = true, + automargin = true, + ), + xaxis_tickangle = -22.5, + yaxis = attr(title = "Number of coins", showgrid = true, zeroline = true), + height = 500, + width = 1000, + ) + layout2 = Layout(; + title = "Exchange volume data (24h) for $(currencies[pair_ID])", + xaxis = attr( + title = "", + showgrid = true, + zeroline = true, + automargin = true, + ), + xaxis_tickangle = -22.5, + yaxis = attr(title = "Volume in USD", showgrid = true, zeroline = true), + height = 500, + width = 1000, + ) + + P1 = Plot(t1, layout1) # plots coin volume data for a given currency + P2 = Plot(t2, layout2) # plots USD volume data for a given currency + return [P1 P2] + + elseif mode_ID == 11 + + t_all = plot_overall_vol_data(duration_ID) + + layout1 = Layout(; + title = "Overall historical volume data (24h) for top 10 exchanges", + xaxis = attr( + title = "", + showgrid = true, + zeroline = true, + automargin = true, + ), + xaxis_tickangle = 0.0, + yaxis = attr(title = "Volume in BTC", showgrid = true, zeroline = true), + height = 500, + width = 1000, + barmode = "stack", + ) + + P1 = Plot(t_all, layout1) # plots overall BTC volume data for all exchanges + return P1 + + end + end + + # Allows access from a web browser, port can be changed to any valid entry + run_server(app, "0.0.0.0", port) +end \ No newline at end of file diff --git a/src/getdataAV.jl b/src/getdataAV.jl new file mode 100644 index 0000000..3e92c79 --- /dev/null +++ b/src/getdataAV.jl @@ -0,0 +1,90 @@ +################# Functions for AlphaVantage API ################# + +function get_price_data_single(currency::String) + + date = Dates.today() + + filename = "$(currency)_EUR_data_$(date).csv" + filepath = joinpath("data", filename) + + df_raw = DataFrame() + + # Look for present day's CSV file, if not found, download and save data to a new file + if isfile(filepath) + @info "Reading $(currency) price/vol data from CSV file on disk" + df_raw = CSV.File(filepath) |> DataFrame + else + try + @info "Fetching $(currency) price/vol data from Alpha Vantage" + raw = AlphaVantage.digital_currency_daily(currency, "EUR", datatype = "csv") + df_raw = raw_to_df(raw) + CSV.write(filepath, df_raw) + catch + @info "Could not fetch data, try again later!" + end + end + + # Return processed DataFrame only when raw data has been fetched successfully + if ~isempty(df_raw) + df_out_price, df_out_candle = average_df_price(currency, df_raw) + df_out_vol = df_vol(currency, df_raw) + + return df_out_price, df_out_candle, df_out_vol + else + return DataFrame[], DataFrame[], DataFrame[] + end +end + +function get_ratings_data(currency::String) + + date = Dates.today() + + filename = "$(currency)_metrics_data_$(date).csv" + filepath = joinpath("data", filename) + + metrics_df = DataFrame() + + # Look for present day's CSV file, if not found, download and save data to a new file + if isfile(filepath) + @info "Reading $(currency) FCAS data from CSV file on disk" + metrics_df = CSV.File(filepath) |> DataFrame + else + try + @info "Fetching $(currency) FCAS data from Alpha Vantage" + rating = AlphaVantage.crypto_rating(currency) + scores = rating["Crypto Rating (FCAS)"] + CSV.write(filepath, scores) + metrics_df = CSV.File(filepath) |> DataFrame + catch err + if isa(err, KeyError) + @info "Could not retrieve data. Something wrong with the API, try again later!" + else + @info "This is a new error: $(err)" + end + end + end + + ratings = ["utility", "fcas score", "developer", "market", "fcas rating"] + index = Array{Int64}(undef, 0) + + # Return scores only when metrics data has been fetched successfully + if ~isempty(metrics_df) + + # Find the row(index) of a string match + for rating in ratings + i = findall(occursin.(rating, metrics_df[!, 1])) + push!(index, i[1]) + end + + # Variables should be assigned in the same order as the list of ratings above + utility_score, fcas_score, dev_score = metrics_df[!, 2][index[1]], + metrics_df[!, 2][index[2]], + metrics_df[!, 2][index[3]] + mark_score, fcas_rating = metrics_df[!, 2][index[4]], metrics_df[!, 2][index[5]] + + return utility_score, fcas_score, dev_score, mark_score, fcas_rating + else + return Number[], Number[], Number[], Number[], Number[] + end + +end \ No newline at end of file diff --git a/src/GetDataFunctions.jl b/src/getdataCG.jl similarity index 54% rename from src/GetDataFunctions.jl rename to src/getdataCG.jl index 3a9e916..72829ac 100644 --- a/src/GetDataFunctions.jl +++ b/src/getdataCG.jl @@ -1,100 +1,12 @@ -function get_price_data_single(currency::String) - - date = Dates.today() - - filename = "$(currency)_EUR_data_$(date).csv" - filepath = joinpath("data", filename) - - raw_df = DataFrame() - - # Look for present day's CSV file, if not found, download and save data to a new file - if isfile(filepath) - @info "Reading $(currency) price/vol data from CSV file on disk" - raw_df = CSV.File(filepath) |> DataFrame - else - try - @info "Fetching $(currency) price/vol data from Alpha Vantage" - raw = AlphaVantage.digital_currency_daily(currency, "EUR", datatype="csv") - raw_df = raw_to_df(raw) - CSV.write(filepath, raw_df) - catch - @info "Could not fetch data, try again later!" - end - end - - # Return processed DataFrame only when raw data has been fetched successfully - if ~isempty(raw_df) - df_out_price, df_out_candle = average_price_df(currency, raw_df) - df_out_vol = vol_df(currency, raw_df) - - return df_out_price, df_out_candle, df_out_vol - else - return DataFrame[], DataFrame[], DataFrame[] - end -end - -function get_ratings_data(currency::String) - - date = Dates.today() - - filename = "$(currency)_metrics_data_$(date).csv" - filepath = joinpath("data", filename) - - metrics_df = DataFrame() - - # Look for present day's CSV file, if not found, download and save data to a new file - if isfile(filepath) - @info "Reading $(currency) FCAS data from CSV file on disk" - metrics_df = CSV.File(filepath) |> DataFrame - else - try - @info "Fetching $(currency) FCAS data from Alpha Vantage" - rating = AlphaVantage.crypto_rating(currency) - scores = rating["Crypto Rating (FCAS)"] - CSV.write(filepath, scores) - metrics_df = CSV.File(filepath) |> DataFrame - catch err - if isa(err, KeyError) - @info "Could not retrieve data. Something wrong with the API, try again later!" - else - @info "This is a new error: $(err)" - end - end - end - - ratings = ["utility", "fcas score", "developer", "market", "fcas rating"] - index = Array{Int64}(undef,0) - - # Return scores only when metrics data has been fetched successfully - if ~isempty(metrics_df) - - # Find the row(index) of a string match - for rating in ratings - i = findall(occursin.(rating, metrics_df[!,1])) - push!(index, i[1]) - end - - # Variables should be assigned in the same order as the list of ratings above - utility_score, fcas_score, dev_score = metrics_df[!,2][index[1]], metrics_df[!,2][index[2]], metrics_df[!,2][index[3]] - mark_score, fcas_rating = metrics_df[!,2][index[4]], metrics_df[!,2][index[5]] - - return utility_score, fcas_score, dev_score, mark_score, fcas_rating - else - return Number[], Number[], Number[], Number[], Number[] - end - -end - - ################# Functions for CoinGecko API ################# function get_API_response(params::String, url::String = URL) - - CG_request = HTTP.request("GET", url * params; verbose = 0, retries = 2) - response_text = String(CG_request.body) - response_dict = JSON.parse(response_text) - - return response_dict + + CG_request = HTTP.request("GET", url * params; verbose = 0, retries = 2) + response_text = String(CG_request.body) + response_dict = JSON.parse(response_text) + + return response_dict end function get_coin_id(currency::String) @@ -109,17 +21,17 @@ function get_coin_id(currency::String) # Look for present day's CSV file, if not found, download and save data to a new file if isfile(filepath) @info "Reading list of coins from CSV file on disk" - df_coins = CSV.File(filepath) |> DataFrame + df_coins = CSV.File(filepath) |> DataFrame else - try - @info "Fetching list of coins from CoinGecko" + try + @info "Fetching list of coins from CoinGecko" coins_dict = get_API_response("/coins/list") - df_coins = vcat(DataFrame.(coins_dict)...) - CSV.write(filepath, df_coins) + df_coins = vcat(DataFrame.(coins_dict)...) + CSV.write(filepath, df_coins) catch - @info "Could not fetch data, try again later!" - end - end + @info "Could not fetch data, try again later!" + end + end # Return valid coin id only when list of coins is available if ~isempty(df_coins) @@ -132,17 +44,15 @@ function get_coin_id(currency::String) # which do not have "-" in them if size(df_filter)[1] > 1 - df_filter_1 = df_filter |> - @filter(~occursin("-", _.id)) |> DataFrame + df_filter_1 = df_filter |> @filter(~occursin("-", _.id)) |> DataFrame if isempty(df_filter_1) - df_filter_1 = df_filter |> - @filter(~occursin("-", _.name)) |> DataFrame + df_filter_1 = df_filter |> @filter(~occursin("-", _.name)) |> DataFrame end return df_filter_1[!, :id][1] end - + return df_filter[!, :id][1] catch err @@ -154,17 +64,17 @@ function get_coin_id(currency::String) end else - return "" + return "" end end function dict_to_df(data_dict::Dict, df::DataFrame) # Collect only the key-value data which is suitable for plotting - for key in collect(keys(data_dict)) + for key in collect(keys(data_dict)) if ~isnothing(data_dict[key]) && length(data_dict[key]) == 1 push!(df, [key Float64(data_dict[key])]) - end + end end return df @@ -177,16 +87,16 @@ function get_dev_comm_data(currency::String) coin_dict, dev_dict, comm_dict = [Dict() for i = 1:3] try - @info "Fetching coin data from CoinGecko" - coin_dict = get_API_response("/coins/$(coin_id)") + @info "Fetching coin data from CoinGecko" + coin_dict = get_API_response("/coins/$(coin_id)") catch - @info "Could not fetch data, try again later!" + @info "Could not fetch data, try again later!" end # Get developer data try dev_dict = coin_dict["developer_data"] - catch err + catch err if isa(err, KeyError) @info "Could not find developer data!" else @@ -197,7 +107,7 @@ function get_dev_comm_data(currency::String) # Get community data try comm_dict = coin_dict["community_data"] - catch err + catch err if isa(err, KeyError) @info "Could not find community data!" else @@ -209,11 +119,11 @@ function get_dev_comm_data(currency::String) df_dev, df_comm = [DataFrame(Metric = String[], Value = Float64[]) for i = 1:2] if ~isempty(dev_dict) - df_dev = dict_to_df(dev_dict, df_dev) + df_dev = dict_to_df(dev_dict, df_dev) end - if ~isempty(comm_dict) - df_comm = dict_to_df(comm_dict, df_comm) + if ~isempty(comm_dict) + df_comm = dict_to_df(comm_dict, df_comm) end return df_dev, df_comm @@ -231,28 +141,30 @@ function get_list_of_exchanges(num_exchanges::Int64) # Look for present day's CSV file, if not found, download and save data to a new file if isfile(filepath) @info "Reading list of exchanges from CSV file on disk" - df_ex = CSV.File(filepath) |> DataFrame + df_ex = CSV.File(filepath) |> DataFrame else - try - @info "Fetching list of exchanges from CoinGecko" + try + @info "Fetching list of exchanges from CoinGecko" ex_dict = get_API_response("/exchanges") - df_all_ex = vcat(DataFrame.(ex_dict)...) + df_all_ex = vcat(DataFrame.(ex_dict)...) # Keep only name and id columns df_ex = DataFrame(name = df_all_ex[!, :name], id = df_all_ex[!, :id]) - CSV.write(filepath, df_ex) + CSV.write(filepath, df_ex) catch err @info "Could not fetch data, try again later!" - @info "$(err)" - end - end + @info "$(err)" + end + end # Return filtered list of exchanges only when data is available if ~isempty(df_ex) - df_ex_vol = DataFrame(Name = df_ex[!, :name][1:num_exchanges], - ID = df_ex[!, :id][1:num_exchanges]) + df_ex_vol = DataFrame( + Name = df_ex[!, :name][1:num_exchanges], + ID = df_ex[!, :id][1:num_exchanges], + ) - return df_ex_vol + return df_ex_vol else return DataFrame() end @@ -263,9 +175,9 @@ function get_exchange_vol_data(currency::String, num_exchanges::Int64) coin_id = get_coin_id(currency) df_ex_vol = get_list_of_exchanges(num_exchanges) - allowmissing!(df_ex_vol) + allowmissing!(df_ex_vol) - exchange_coin_vol, exchange_usd_vol = [Union{Missing, Float64}[] for i = 1:2] + exchange_coin_vol, exchange_usd_vol = [Union{Missing,Float64}[] for i = 1:2] # Extract volume data only when a list of exchanges is available if ~isempty(df_ex_vol) @@ -275,20 +187,24 @@ function get_exchange_vol_data(currency::String, num_exchanges::Int64) coin_vol, usd_vol = [Float64[] for i = 1:2] coin_vol_tickers_dict = Dict() - try - coin_vol_tickers_dict = get_API_response("/exchanges/$(exchange)/tickers?coin_ids=$(coin_id)") + try + coin_vol_tickers_dict = + get_API_response("/exchanges/$(exchange)/tickers?coin_ids=$(coin_id)") catch err @info "Could not find $(coin_id) volume data on $(exchange)!" @info "$(err)" end - - if ~isempty(coin_vol_tickers_dict) - - for i = 1:length(coin_vol_tickers_dict["tickers"]) + + if ~isempty(coin_vol_tickers_dict) + + for i = 1:length(coin_vol_tickers_dict["tickers"]) push!(coin_vol, coin_vol_tickers_dict["tickers"][i]["volume"]) - push!(usd_vol, coin_vol_tickers_dict["tickers"][i]["converted_volume"]["usd"]) + push!( + usd_vol, + coin_vol_tickers_dict["tickers"][i]["converted_volume"]["usd"], + ) end - + push!(exchange_coin_vol, sum(coin_vol)) push!(exchange_usd_vol, sum(usd_vol)) @@ -297,11 +213,16 @@ function get_exchange_vol_data(currency::String, num_exchanges::Int64) push!(exchange_usd_vol, missing) end - end + end end - insertcols!(df_ex_vol, 3, :Coin_volume => exchange_coin_vol, :USD_volume => exchange_usd_vol) - + insertcols!( + df_ex_vol, + 3, + :Coin_volume => exchange_coin_vol, + :USD_volume => exchange_usd_vol, + ) + return df_ex_vol end @@ -327,11 +248,12 @@ function get_vol_chart(exchange::String) else try # Fetch and save data for 365 days - ex_vol_chart = get_API_response("/exchanges/$(exchange)/volume_chart?days=$(days)") + ex_vol_chart = + get_API_response("/exchanges/$(exchange)/volume_chart?days=$(days)") open(filepath, "w") do f for i = 1:length(ex_vol_chart) - writedlm(f, [ex_vol_chart[i]], ";") + writedlm(f, [ex_vol_chart[i]], ";") end end @@ -341,23 +263,23 @@ function get_vol_chart(exchange::String) end end - if length(ex_vol_chart) > days - start_index = Int64(length(ex_vol_chart)/2) + 1 + if length(ex_vol_chart) > days + start_index = Int64(length(ex_vol_chart) / 2) + 1 return ex_vol_chart[start_index:end] else - ex_vol = Union{Missing, Float64}[] + ex_vol = Union{Missing,Float64}[] for i = 1:length(ex_vol_chart) try - push!(ex_vol, round(parse(Float64, ex_vol_chart[i][2]); digits = 2)) + push!(ex_vol, round(parse(Float64, ex_vol_chart[i][2]); digits = 2)) catch push!(ex_vol, missing) - end - end + end + end return ex_vol - end + end end function get_overall_vol_data(duration::Int64, num_exchanges::Int64) @@ -371,12 +293,12 @@ function get_overall_vol_data(duration::Int64, num_exchanges::Int64) # Create a column with dates f_day = Dates.today() - i_day = f_day - Dates.Day(duration-1) + i_day = f_day - Dates.Day(duration - 1) time = collect(i_day:Dates.Day(1):f_day) df_ex_vol = DataFrame(Time = time) - allowmissing!(df_ex_vol) + allowmissing!(df_ex_vol) # Extract volume data only when a list of exchanges is available if ~isempty(df_ex_list) @@ -384,7 +306,7 @@ function get_overall_vol_data(duration::Int64, num_exchanges::Int64) for exchange in df_ex_list[!, :ID] ex_vol = Vector{Any}[] - + try ex_vol = get_vol_chart(exchange) catch err @@ -393,20 +315,20 @@ function get_overall_vol_data(duration::Int64, num_exchanges::Int64) # Skip next part of the code, and continue to next exchange continue - end + end # Check and filter on duration if duration > length(ex_vol) duration = length(ex_vol) - end - ex_vol = ex_vol[end-duration+1:end] + end + ex_vol = ex_vol[end-duration+1:end] # Find name of the exchange corresponding to its ID df_row = df_ex_list |> @filter(_.ID == exchange) |> DataFrame name = df_row[!, :Name][1] try - insertcols!(df_ex_vol, 2, Symbol(name) => ex_vol) + insertcols!(df_ex_vol, 2, Symbol(name) => ex_vol) catch err if isa(err, DimensionMismatch) @info "Data is missing for $(exchange) for the requested duration" @@ -414,7 +336,7 @@ function get_overall_vol_data(duration::Int64, num_exchanges::Int64) @info "Something went wrong, check this error: $(err)" end end - + end return df_ex_vol @@ -422,4 +344,4 @@ function get_overall_vol_data(duration::Int64, num_exchanges::Int64) return DataFrame() end -end \ No newline at end of file +end \ No newline at end of file diff --git a/src/helper.jl b/src/helper.jl new file mode 100644 index 0000000..0b3133b --- /dev/null +++ b/src/helper.jl @@ -0,0 +1,158 @@ +################# Helper functions ################# + +function raw_to_df(raw_data) + + # Argument ":auto" is required to generate column names in latest version of DataFrames.jl (v1.2.1) + df = DataFrame(raw_data[1], :auto) + df_names = Symbol.(vcat(raw_data[2]...)) + df = DataFrames.rename(df, df_names) + + timestamps = df[!, :timestamp] + + select!(df, Not([:timestamp])) + + for col in eachcol(df) + col = Float64.(col) + end + + df[!, :Date] = Date.(timestamps) + return df +end + +function average_df_price(currency::String, df_in::DataFrame) + + df_out_price, df_out_candle = [DataFrame() for i = 1:2] + + df_out_price[!, :Date] = df_in[!, :Date] + + df_out_price[!, Symbol("$currency")] = + ( + df_in[!, Symbol("open (EUR)")] + + df_in[!, Symbol("high (EUR)")] + + df_in[!, Symbol("low (EUR)")] + + df_in[!, Symbol("close (EUR)")] + ) / 4 + + candle_col = Any[] + for i = 1:size(df_in)[1] + push!( + candle_col, + ( + df_in[!, Symbol("open (EUR)")][i], + df_in[!, Symbol("high (EUR)")][i], + df_in[!, Symbol("low (EUR)")][i], + df_in[!, Symbol("close (EUR)")][i], + ), + ) + end + + df_out_candle[!, :Date] = df_in[!, :Date] + + df_out_candle[!, Symbol("$currency")] = candle_col + + return df_out_price, df_out_candle +end + +function df_vol(currency::String, df_in::DataFrame) + + df_out_vol = DataFrame() + + df_out_vol[!, :Date] = df_in[!, :Date] + df_out_vol[!, Symbol("$currency")] = df_in[!, :volume] + + return df_out_vol +end + +function moving_averages(df_price::DataFrame, duration::Int64, window::Int64) + + # df_price should have date order - oldest to latest + Price_col = df_price[end-duration+1-window+1:end, 2] + rows1 = length(Price_col) + price_SMA, price_WMA, price_EMA = [Float64[] for i = 1:3] + + weights = collect(1:window) / (window * (window + 1) / 2) + k = 2 / (window + 1) + + # Calculate different Moving Averages + for i = 1:rows1-(window-1) + # Simple Moving Average (SMA) + push!(price_SMA, mean(Price_col[i:i+(window-1)])) + + # Weighted Moving Average + push!(price_WMA, sum(Price_col[i:i+(window-1)] .* weights)) + + # Exponential Moving Average + SMA = mean(Price_col[i:i+(window-1)]) + EMA = (Price_col[i+(window-1)] * k) + (SMA * (1 - k)) + push!(price_EMA, EMA) + end + + return price_SMA, price_WMA, price_EMA +end + +function moving_std(df_price::DataFrame, duration::Int64, window::Int64) + + # df_price should have date order - oldest to latest + Price_col = df_price[end-duration+1-window+1:end, 2] + rows1 = length(Price_col) + + Price_std = Float64[] + + for i = 1:rows1-(window-1) + # Standard deviation over the period SMA is also being calculated + push!(Price_std, std(Price_col[i:i+(window-1)])) + end + + return Price_std +end + +function calculate_ema(Price_col::Vector{Float64}, window::Int64) + + k(window) = 2 / (window + 1) + price_EMA = Float64[] + rows = length(Price_col) + + for i = 1:rows-(window-1) + + SMA = mean(Price_col[i:i+(window-1)]) + EMA = (Price_col[i+(window-1)] * k(window)) + (SMA * (1 - k(window))) + push!(price_EMA, EMA) + end + + return price_EMA +end + +function calculate_macd( + df_price::DataFrame, + window_long::Int64 = 26, + window_short::Int64 = 12, + window_signal::Int64 = 9, +) + + # df_price should have date order - oldest to latest + Price_col = df_price[!, 2] + + # Calculate 26 (long) and 12 (short) period EMA + price_EMA_short = calculate_ema(Price_col, window_short) + price_EMA_long = calculate_ema(Price_col, window_long) + + # Make EMA_long and EMA_short equal + EMA_short_col = price_EMA_short[window_long-window_short+1:end] + + # Calculate MACD = EMA_short - EMA_long + MACD_col = EMA_short_col - price_EMA_long + + # Calculate signal line (9 period EMA of MACD) + Signal_col = calculate_ema(MACD_col, window_signal) + + df_ema = DataFrame( + date = df_price[window_long+window_signal-1:end, :Date], + Raw = df_price[window_long+window_signal-1:end, 2], + EMA_long = price_EMA_long[window_signal:end], + EMA_short = EMA_short_col[window_signal:end], + MACD = MACD_col[window_signal:end], + Signal = Signal_col, + ) + + return df_ema +end \ No newline at end of file diff --git a/src/plotsAV.jl b/src/plotsAV.jl new file mode 100644 index 0000000..c524d20 --- /dev/null +++ b/src/plotsAV.jl @@ -0,0 +1,391 @@ +################# Plots for AlphaVantage data ################# + +function plot_price_ma_trade_data(index::Int64, duration::Int64, window::Int64) + # Retrieve data from various helper functions + df_price, _, df_vol = get_price_data_single(currencies[index]) + + # Make sure that duration does not exceed the number of rows - max(windows) in the DataFrame + # This allows calculation of MA for the longest duration + if duration > size(df_price)[1] - maximum(windows) + duration = size(df_price)[1] - maximum(windows) + end + + ################# Daily average data ################# + trace1 = PlotlyJS.scatter(; + x = df_price[1:duration, :Date], + y = df_price[1:duration, 2], + mode = "markers+lines", + name = "$(currencies[index]) price", + ) + + ################# Moving averages data ################# + sort!(df_price, :Date) # oldest date first, newest at the bottom + price_SMA, price_WMA, price_EMA = moving_averages(df_price, duration, window) + + trace2 = PlotlyJS.scatter(; + x = df_price[!, :Date][end-length(price_SMA)+1:end], + y = price_SMA, + mode = "lines", + name = "$(names(df_price)[2]) SMA over $(window) days", + ) + trace3 = PlotlyJS.scatter(; + x = df_price[!, :Date][end-length(price_WMA)+1:end], + y = price_WMA, + mode = "lines", + name = "$(names(df_price)[2]) WMA over $(window) days", + ) + trace4 = PlotlyJS.scatter(; + x = df_price[!, :Date][end-length(price_EMA)+1:end], + y = price_EMA, + mode = "lines", + name = "$(names(df_price)[2]) EMA over $(window) days", + ) + + ################# Daily trade data ################# + sort!(df_price, :Date, rev = true) # newest date first, oldest at the bottom + trace5 = PlotlyJS.bar(; + x = df_price[1:duration, :Date], + y = df_price[1:duration, 2] .* df_vol[1:duration, 2], + mode = "markers+lines", + name = "$(currencies[index]) daily trade", + ) + + return trace1, trace2, trace3, trace4, trace5 +end + +function plot_price_bollinger_bands(index::Int64, duration::Int64, window::Int64) + + df_price, _, _ = get_price_data_single(currencies[index]) + + if duration > size(df_price)[1] - maximum(windows) + duration = size(df_price)[1] - maximum(windows) + end + + sort!(df_price, :Date) # oldest date first, newest at the bottom + price_SMA, _, _ = moving_averages(df_price, duration, window) + Price_σ = moving_std(df_price, duration, window) + + ################# Raw price data ################# + + trace1 = PlotlyJS.scatter(; + x = df_price[!, :Date][end-length(price_SMA)+1:end], + y = df_price[!, 2][end-length(price_SMA)+1:end], + mode = "markers+lines", + name = "$(currencies[index]) price", + ) + + ################# SMA and Bollinger bands ################# + + trace2 = PlotlyJS.scatter(; + x = df_price[!, :Date][end-length(price_SMA)+1:end], + y = price_SMA, + mode = "lines", + name = "$(names(df_price)[2]) SMA over $(window) days", + ) + + trace3 = PlotlyJS.scatter(; + x = df_price[!, :Date][end-length(price_SMA)+1:end], + y = price_SMA .+ 2 * Price_σ, + mode = "markers", + name = "Upper band (+2σ)", + ) + + trace4 = PlotlyJS.scatter(; + x = df_price[!, :Date][end-length(price_SMA)+1:end], + y = price_SMA .- 2 * Price_σ, + mode = "markers", + name = "Lower band (-2σ)", + ) + + return trace1, trace2, trace3, trace4 +end + +function plot_candle_vol_data(index::Int64, duration::Int64) + + # Retrieve data from various helper functions + _, Candle_df, df_vol = get_price_data_single(currencies[index]) + + # Make sure that duration does not exceed the number of rows in the DataFrame + if duration > size(Candle_df)[1] + duration = size(Candle_df)[1] + end + + ################# Daily candlestick data ################# + open_col, high_col, low_col, close_col = [Float64[] for i = 1:4] + + for i = 1:duration + push!(open_col, Candle_df[!, 2][i][1]) + push!(high_col, Candle_df[!, 2][i][2]) + push!(low_col, Candle_df[!, 2][i][3]) + push!(close_col, Candle_df[!, 2][i][4]) + end + + trace1 = PlotlyJS.candlestick(; + x = Candle_df[1:duration, :Date], + open = open_col, + high = high_col, + low = low_col, + close = close_col, + name = "$(currencies[index])", + ) + + + ################# Daily volume data ################# + trace2 = PlotlyJS.bar(; + x = df_vol[1:duration, :Date], + y = df_vol[1:duration, 2], + name = "$(currencies[index]) volume", + ) + + return trace1, trace2 +end + +function plot_cumul_daily_return_hist(index::Int64, duration::Int64) + + # Retrieve data from various helper functions + df_price, _, _ = get_price_data_single(currencies[index]) + + # Make sure that duration does not exceed the number of rows in the DataFrame + if duration > size(df_price)[1] + duration = size(df_price)[1] + end + + # Reverse the order (oldest date first, newest at the bottom) + sort!(df_price, :Date) + + ################# Cumulative return ################# + + trace1 = PlotlyJS.scatter(; + x = df_price[end-duration+2:end, :Date], + y = ( + cumsum(diff(df_price[end-duration+1:end, 2])) ./ df_price[end-duration+1, 2] + ) .* 100, + mode = "markers+lines", + name = "$(currencies[index]) cumulative return", + ) + + ################# Daily return ################# + + X = df_price[end-duration+2:end, :Date] + Y = (diff(df_price[end-duration+1:end, 2]) ./ df_price[end-duration+1:end-1, 2]) .* 100 + + # Split into two datasets (green: positive change, red: negative change) + green_Y = Y[Y.≥0.0] + green_X = X[Y.≥0.0] + + red_Y = Y[Y.<0.0] + red_X = X[Y.<0.0] + + green_share = round((length(green_Y) / length(Y)) * 100, digits = 2) + red_share = 100.0 - green_share + + trace2_green = PlotlyJS.bar(; + x = green_X, + y = green_Y, + marker_color = "green", + name = "$(currencies[index]) increase, share = $(green_share) %", + ) + trace2_red = PlotlyJS.bar(; + x = red_X, + y = red_Y, + marker_color = "red", + name = "$(currencies[index]) decrease, share = $(red_share) %", + ) + + ################# Daily change histogram / Daily volatility ################# + + σ = round(std(Y), digits = 2) + trace3 = plotly_hist(Y, 75; normalize = false) + + return trace1, trace2_green, trace2_red, trace3, σ, duration +end + +function plotly_hist( + x::AbstractVector{T}, + nbins::Integer; + normalize::Bool = true, +) where {T<:Number} + + # use StatsBase to create a histogram object + hist = StatsBase.fit(Histogram, x, nbins = nbins, closed = :left) + + # obtain bar positions -> center of each interval + bins = similar(x, length(hist.edges[1]) - 1) + edges = hist.edges[1] + + for k in eachindex(bins) + bins[k] = (edges[k] + edges[k+1]) * 0.5 + end + + if normalize + y = hist.weights ./ length(x) # we need a new array + else + y = hist.weights + end + + trace = bar(x = bins, y = y) + + return trace +end + +function plot_fcas_data(index::Int64) + + us, fs, ds, ms, fr = get_ratings_data(currencies[index]) + + ################# FCAS metrics data ################# + trace1 = PlotlyJS.bar(; + x = ["Utility", "FCAS", "Developer", "Market maturity"], + y = [us, fs, ds, ms], + width = 0.25, + ) + return trace1, fr +end + +function plot_macd_signal(index::Int64, duration::Int64) + + # Retrieve data from various helper functions + df_price, _, _ = get_price_data_single(currencies[index]) + + # Make sure that duration does not exceed the number of rows in the DataFrame + if duration > size(df_price)[1] + duration = size(df_price)[1] + end + + sort!(df_price, :Date) # oldest date first, newest at the bottom + + df_price = df_price[end-duration+1-26-9+1:end, :] # filter based on selected duration and effective + # window size of 26+9 + + df_ema_all = calculate_macd(df_price) # get EMA and MACD data into a DataFrame + + + ################# Daily average, EMA-12 and EMA-26 data ################# + trace1 = PlotlyJS.scatter(; + x = df_ema_all[!, :date], + y = df_ema_all[!, :Raw], + mode = "markers+lines", + name = "Daily average", + ) + + trace2 = PlotlyJS.scatter(; + x = df_ema_all[!, :date], + y = df_ema_all[!, :EMA_long], + mode = "markers+lines", + name = "EMA-26", + ) + + trace3 = PlotlyJS.scatter(; + x = df_ema_all[!, :date], + y = df_ema_all[!, :EMA_short], + mode = "markers+lines", + name = "EMA-12", + ) + + + ################# MACD and its signal data ################# + trace4 = PlotlyJS.scatter(; + x = df_ema_all[!, :date], + y = df_ema_all[!, :MACD], + mode = "markers+lines", + name = "MACD", + ) + + trace5 = PlotlyJS.scatter(; + x = df_ema_all[!, :date], + y = df_ema_all[!, :Signal], + mode = "markers+lines", + name = "Signal (EMA-9)", + ) + + + ################# Distance between MACD and its signal line ################# + + X = df_ema_all[!, :date] + Y = df_ema_all[!, :MACD] - df_ema_all[!, :Signal] + + # Split into two datasets (green: positive change, red: negative change) + green_Y = Y[Y.≥0.0] + green_X = X[Y.≥0.0] + + red_Y = Y[Y.<0.0] + red_X = X[Y.<0.0] + + trace6_green = PlotlyJS.bar(; + x = green_X, + y = green_Y, + marker_color = "green", + name = "MACD above signal ", + ) + + trace6_red = PlotlyJS.bar(; + x = red_X, + y = red_Y, + marker_color = "red", + name = "MACD below signal", + ) + + return trace1, trace2, trace3, trace4, trace5, trace6_green, trace6_red + +end + +function plot_linear_regression(index::Int64, duration::Int64) + + # Retrieve data from various helper functions + df_price, _, _ = get_price_data_single(currencies[index]) + + # Make sure that duration does not exceed the number of rows in the DataFrame + if duration > size(df_price)[1] + duration = size(df_price)[1] + end + + # Filter on duration and create index column (model fit does not work with dates) + df_fit = sort!(df_price[1:duration, :]) + df_fit.Index = 1:size(df_fit)[1] + + # Rename column to price to make df_fit generic + rename!(df_fit, Dict(Symbol(names(df_fit)[2]) => "Price")) + + # Get model parameters and R² + model_params = lm(@formula(Price ~ Index), df_fit) + R² = round(r2(model_params), digits = 2) + + # Predicted data + price_from_model = Float64.(predict(model_params, df_fit)) + + # Get standard deviation σ + σ = std(price_from_model) + + ################# Raw price data ################# + + trace1 = PlotlyJS.scatter(; + x = df_fit[!, :Date], + y = df_fit[!, :Price], + mode = "markers+lines", + name = "Actual price", + ) + + ################# Linear regression channel ################# + + trace2 = PlotlyJS.scatter(; + x = df_fit[!, :Date], + y = price_from_model, + mode = "lines", + name = "Linear regression line", + ) + + trace3 = PlotlyJS.scatter(; + x = df_fit[!, :Date], + y = price_from_model .+ 2 * σ, + mode = "markers", + name = "Upper channel (+2σ)", + ) + + trace4 = PlotlyJS.scatter(; + x = df_fit[!, :Date], + y = price_from_model .- 2 * σ, + mode = "markers", + name = "Lower channel (-2σ)", + ) + + return trace1, trace2, trace3, trace4, R² +end \ No newline at end of file diff --git a/src/plotsCG.jl b/src/plotsCG.jl new file mode 100644 index 0000000..ae9a25e --- /dev/null +++ b/src/plotsCG.jl @@ -0,0 +1,74 @@ +################# Plots for CoinGecko data ################# + +function plot_dev_comm_data(index::Int64) + + # Convert currency symbol to lowercase and fetch data from CoinGecko + df_dev, df_comm = get_dev_comm_data(lowercase(currencies[index])) + + ################# Developer data ################# + trace1 = PlotlyJS.bar(; + x = df_dev[!, :Metric], + y = df_dev[!, :Value], + name = "Developer data", + ) + + ################# Community data ################# + trace2 = PlotlyJS.bar(; + x = df_comm[!, :Metric], + y = df_comm[!, :Value], + name = "Community data", + ) + + return trace1, trace2 +end + +function plot_exchange_vol_data(index::Int64, num_exchanges::Int64 = 10) + + # Convert currency symbol to lowercase and fetch data from CoinGecko + df_ex_vol = get_exchange_vol_data(lowercase(currencies[index]), num_exchanges) + + ################# Coin volume data ################# + trace1 = PlotlyJS.bar(; + x = df_ex_vol[!, :Name], + y = df_ex_vol[!, :Coin_volume], + name = "Volume data in coins", + ) + + ################# USD volume data ################# + trace2 = PlotlyJS.bar(; + x = df_ex_vol[!, :Name], + y = df_ex_vol[!, :USD_volume], + name = "Volume data in USD", + ) + + return trace1, trace2 +end + +function plot_overall_vol_data(duration::Int64, num_exchanges::Int64 = 10) + + # Collect all traces for all exchanges + all_traces = GenericTrace{Dict{Symbol,Any}}[] + + # Fetch overall volume data from CoinGecko for given historical duration + df_ex_vol = get_overall_vol_data(duration, num_exchanges) + + if ~isempty(df_ex_vol) + + # First column is for duration + exchanges = names(df_ex_vol)[2:end] + + for i = 1:length(exchanges) + + trace = PlotlyJS.bar(; + x = df_ex_vol[!, :Time], + y = df_ex_vol[!, i+1], + mode = "markers+lines", + name = "$(exchanges[i])", + ) + + push!(all_traces, trace) + end + end + + return all_traces +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 27be9a0..df0f53f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,11 +15,12 @@ CryptoDashApp.remove_old_files() ################# Test cases for accessing market data ################# -@testset "Check if AV market data are accessible" begin +@testset "Check if AV market data are accessible" begin for currency in ["ETH", "LTC", "LINK"] - df_out_price, df_out_candle, df_out_vol = CryptoDashApp.get_price_data_single(currency) + df_out_price, df_out_candle, df_out_vol = + CryptoDashApp.get_price_data_single(currency) @test ~isempty(df_out_price) @test ~isempty(df_out_candle) @@ -29,12 +30,14 @@ CryptoDashApp.remove_old_files() end -@testset "Check for exception handling while accessing AV market data" begin +@testset "Check for exception handling while accessing AV market data" begin currency = "dummy" - - @test_logs (:info, "Could not fetch data, try again later!") match_mode=:any CryptoDashApp.get_price_data_single(currency) - + + @test_logs (:info, "Could not fetch data, try again later!") match_mode = :any CryptoDashApp.get_price_data_single( + currency, + ) + end ################# Test cases for moving averages ################# @@ -43,16 +46,17 @@ end for currency in ["BTC", "DOT"] - df_out_price, _ , _ = CryptoDashApp.get_price_data_single(currency) + df_out_price, _, _ = CryptoDashApp.get_price_data_single(currency) duration = 90 window = 30 - Price_SMA, Price_WMA, Price_EMA = CryptoDashApp.moving_averages(df_out_price, duration, window) + price_SMA, price_WMA, price_EMA = + CryptoDashApp.moving_averages(df_out_price, duration, window) - @test ~isempty(Price_SMA) - @test ~isempty(Price_WMA) - @test ~isempty(Price_WMA) + @test ~isempty(price_SMA) + @test ~isempty(price_WMA) + @test ~isempty(price_WMA) df_out_price = df_out_price[end-duration+1-26-9+1:end, :] df_ema_all = CryptoDashApp.calculate_macd(df_out_price) @@ -60,35 +64,37 @@ end Price_σ = CryptoDashApp.moving_std(df_out_price, duration, window) @test ~isempty(Price_σ) - + end - + end ################# Test cases for CoinGecko API ################# -@testset "Check if CG developer and community data are accessible" begin +@testset "Check if CG developer and community data are accessible" begin for currency in ["btc", "eth", "ltc"] - df_dev, df_comm = CryptoDashApp.get_dev_comm_data(currency) + df_dev, df_comm = CryptoDashApp.get_dev_comm_data(currency) @test ~isempty(df_dev) - @test ~isempty(df_comm) + @test ~isempty(df_comm) end end -@testset "Check for exception handling while determining coin id" begin +@testset "Check for exception handling while determining coin id" begin currency = "dummy" - @test_logs (:info, "Could not find an id for the given currency") match_mode=:any CryptoDashApp.get_coin_id(currency) - + @test_logs (:info, "Could not find an id for the given currency") match_mode = :any CryptoDashApp.get_coin_id( + currency, + ) + end -@testset "Check if CG exchange volume data per currency are accessible" begin +@testset "Check if CG exchange volume data per currency are accessible" begin for currency in ["btc", "eth", "ltc"] @@ -96,14 +102,14 @@ end df_ex_vol = CryptoDashApp.get_exchange_vol_data(currency, num_exchanges) - @test size(df_ex_vol)[1] == num_exchanges + @test size(df_ex_vol)[1] == num_exchanges end end -@testset "Check if CG overall exchange volume data are accessible" begin - +@testset "Check if CG overall exchange volume data are accessible" begin + num_exchanges = 10 for duration in [5, 10, 50, 75]