|
import streamlit as st |
|
|
|
import pandas as pd |
|
import yfinance as yf |
|
|
|
import altair as alt |
|
|
|
|
|
def clean_etf_data(df): |
|
""" |
|
Clean ETF data |
|
""" |
|
|
|
df.Date = pd.to_datetime(df.Date, dayfirst=True) |
|
df.set_index("Date", inplace=True) |
|
|
|
df.index = df.index.normalize().date |
|
|
|
df.replace(to_replace=r"\(([0-9.]+)\)", value=r"-\1", regex=True, inplace=True) |
|
|
|
|
|
df_original = df.copy() |
|
|
|
|
|
df.replace("-", 0, inplace=True) |
|
|
|
|
|
df = df.apply(pd.to_numeric) |
|
|
|
return df, df_original |
|
|
|
|
|
|
|
|
|
|
|
btc_etf_flow = pd.read_html( |
|
"https://farside.co.uk/?p=1321", attrs={"class": "etf"}, skiprows=[1] |
|
)[0] |
|
|
|
|
|
|
|
btc_etf_flow = btc_etf_flow.iloc[:-4] |
|
|
|
btc_etf_funds = btc_etf_flow.drop(["Date", "Total"], axis=1).columns |
|
|
|
|
|
eth_etf_flow = pd.read_html( |
|
"https://farside.co.uk/ethereum-etf-flow-all-data/", |
|
attrs={"class": "etf"}, |
|
skiprows=[2, 3], |
|
)[0] |
|
|
|
eth_etf_flow.columns = eth_etf_flow.columns.droplevel(2) |
|
|
|
eth_etf_funds = eth_etf_flow.drop("Total", axis=1).columns[1:].get_level_values(1) |
|
|
|
eth_etf_flow.columns = eth_etf_flow.columns.map(" | ".join) |
|
|
|
eth_etf_flow.rename( |
|
columns={ |
|
"Unnamed: 0_level_0 | Unnamed: 0_level_1": "Date", |
|
"Total | Unnamed: 10_level_1": "Total", |
|
}, |
|
inplace=True, |
|
) |
|
|
|
eth_etf_flow = eth_etf_flow.iloc[:-1] |
|
|
|
btc_etf_flow, btc_etf_flow_original = clean_etf_data(btc_etf_flow) |
|
eth_etf_flow, eth_etf_flow_original = clean_etf_data(eth_etf_flow) |
|
|
|
|
|
|
|
|
|
btc_etf_volumes = pd.DataFrame() |
|
for fund in btc_etf_funds: |
|
btc_etf_volumes[fund] = yf.download( |
|
f"{fund}", interval="1d", period="max", start=btc_etf_flow.index[0] |
|
)["Volume"] |
|
|
|
|
|
btc_etf_volumes.index = btc_etf_volumes.index.normalize().date |
|
|
|
|
|
eth_etf_volumes = pd.DataFrame() |
|
for fund in eth_etf_funds: |
|
eth_etf_volumes[fund] = yf.download( |
|
f"{fund}", interval="1d", period="max", start=eth_etf_flow.index[0] |
|
)["Volume"] |
|
|
|
|
|
eth_etf_volumes.index = eth_etf_volumes.index.normalize().date |
|
|
|
|
|
|
|
|
|
btc_price = yf.download( |
|
"BTC-USD", interval="1d", period="max", start=btc_etf_flow.index[0] |
|
) |
|
btc_price = btc_price.Close |
|
|
|
btc_price.index = btc_price.index.normalize().date |
|
|
|
|
|
eth_price = yf.download( |
|
"ETH-USD", interval="1d", period="max", start=eth_etf_flow.index[0] |
|
) |
|
eth_price = eth_price.Close |
|
|
|
eth_price.index = eth_price.index.normalize().date |
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
st.set_page_config(layout="wide", page_icon="π") |
|
|
|
st.title("Crypto ETF Dashboard") |
|
|
|
|
|
st.sidebar.title("Crypto ETF Dashboard") |
|
|
|
asset = st.sidebar.selectbox("Choose asset", ("BTC", "ETH")) |
|
|
|
|
|
if asset == "BTC": |
|
st.header("BTC ETF") |
|
etf_flow = btc_etf_flow |
|
etf_volumes = btc_etf_volumes |
|
price = btc_price |
|
else: |
|
st.header("ETH ETF") |
|
etf_flow = eth_etf_flow |
|
etf_volumes = eth_etf_volumes |
|
price = eth_price |
|
|
|
etf_flow_individual = etf_flow.drop("Total", axis=1) |
|
etf_flow_total = etf_flow.Total |
|
|
|
|
|
st.subheader(f"{asset} ETF Trading volume") |
|
trading_vol_fig = ( |
|
alt.Chart(etf_volumes.reset_index()) |
|
.transform_fold(etf_volumes.columns, as_=["Funds", "Volume"]) |
|
.mark_bar() |
|
.encode( |
|
x=alt.X("index:T", title="Date"), |
|
y=alt.Y("Volume:Q", title="Volume"), |
|
color="Funds:N", |
|
) |
|
) |
|
st.altair_chart(trading_vol_fig, use_container_width=True) |
|
|
|
|
|
st.subheader(f"{asset} ETF Net flow individual funds") |
|
net_flow_individual_fig = ( |
|
alt.Chart(etf_flow_individual.reset_index()) |
|
.transform_fold( |
|
etf_flow_individual.columns, |
|
as_=["Funds", "Net Flow"], |
|
) |
|
.mark_bar() |
|
.encode( |
|
x=alt.X("index:T", title="Date"), |
|
y=alt.Y("Net Flow:Q", title="Net Flow"), |
|
color="Funds:N", |
|
) |
|
) |
|
st.altair_chart(net_flow_individual_fig, use_container_width=True) |
|
|
|
|
|
st.subheader(f"{asset} ETF Net flow total vs asset price") |
|
net_flow_total_fig = ( |
|
alt.Chart(etf_flow_total.reset_index()) |
|
.mark_bar() |
|
.encode( |
|
x=alt.X("index:T", title="Date"), |
|
y=alt.Y("Total:Q"), |
|
color=alt.condition( |
|
alt.datum.Total > 0, |
|
alt.value("seagreen"), |
|
alt.value("orangered"), |
|
), |
|
) |
|
) |
|
|
|
price_fig = ( |
|
alt.Chart(price.reset_index()) |
|
.mark_line() |
|
.encode( |
|
x=alt.X("index:T", title="Date"), |
|
y=alt.Y("Close:Q", title="Price"), |
|
color=alt.value("crimson"), |
|
) |
|
) |
|
st.altair_chart( |
|
(net_flow_total_fig + price_fig).resolve_scale( |
|
y="independent", |
|
), |
|
use_container_width=True, |
|
) |
|
|
|
|
|
st.subheader(f"{asset} ETF Cumulative flow of individual funds vs asset price") |
|
cum_flow_individual = etf_flow_individual.cumsum() |
|
|
|
cum_flow_individual_net_fig = ( |
|
alt.Chart(cum_flow_individual.reset_index()) |
|
.transform_fold(cum_flow_individual.columns, as_=["Funds", "Net Flow"]) |
|
.mark_area() |
|
.encode( |
|
x=alt.X("index:T", title="Date"), |
|
y=alt.Y("Net Flow:Q", title="Net Flow"), |
|
color=alt.Color("Funds:N").scale(scheme="tableau20"), |
|
) |
|
) |
|
st.altair_chart( |
|
(cum_flow_individual_net_fig + price_fig).resolve_scale(y="independent"), |
|
use_container_width=True, |
|
) |
|
|
|
|
|
st.subheader(f"{asset} ETF Cumulative flow total vs asset price") |
|
cum_flow_total = etf_flow_total.cumsum() |
|
|
|
cum_flow_total_fig = ( |
|
alt.Chart(cum_flow_total.reset_index()) |
|
.transform_calculate( |
|
negative="datum.Total < 0", |
|
) |
|
.mark_area() |
|
.encode( |
|
x=alt.X("index:T"), |
|
y=alt.Y("Total:Q", impute={"value": 0}), |
|
color=alt.Color("negative:N", title="Negative Flow").scale( |
|
scheme="set2" |
|
), |
|
) |
|
) |
|
st.altair_chart( |
|
(cum_flow_total_fig + price_fig).resolve_scale( |
|
y="independent", |
|
), |
|
use_container_width=True, |
|
) |
|
|