|
import pandas as pd |
|
import yfinance as yf |
|
|
|
import streamlit as st |
|
import altair as alt |
|
|
|
from types import SimpleNamespace |
|
|
|
alt.renderers.set_embed_options(theme="dark") |
|
|
|
|
|
def clean_etf_data(df): |
|
""" |
|
Clean ETF data |
|
""" |
|
|
|
df_original = df.copy() |
|
|
|
df_original["Date"] = pd.to_datetime(df_original["Date"]) |
|
|
|
|
|
df = df.drop(columns="Date") |
|
df.replace(to_replace=r"\(([0-9.]+)\)", value=r"-\1", regex=True, inplace=True) |
|
|
|
|
|
df.replace("-", 0, inplace=True) |
|
|
|
|
|
df = df.apply(pd.to_numeric) |
|
df["Date"] = df_original["Date"] |
|
|
|
return df, df_original |
|
|
|
|
|
def extract_date_index(df): |
|
""" |
|
Extract index from dataframe as Date |
|
""" |
|
|
|
if isinstance(df, pd.Series): |
|
df = df.to_frame() |
|
df = df.reset_index(names="Date") |
|
|
|
df.Date = pd.to_datetime(df.Date) |
|
|
|
return df |
|
|
|
|
|
|
|
|
|
|
|
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.to_list() |
|
|
|
|
|
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).to_list() |
|
) |
|
|
|
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 = extract_date_index(btc_etf_volumes) |
|
|
|
|
|
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 = extract_date_index(eth_etf_volumes) |
|
|
|
|
|
|
|
|
|
btc_price = yf.download( |
|
"BTC-USD", interval="1d", period="max", start=btc_etf_flow["Date"][0] |
|
) |
|
btc_price = btc_price.Close |
|
|
|
btc_price = extract_date_index(btc_price) |
|
|
|
|
|
eth_price = yf.download( |
|
"ETH-USD", interval="1d", period="max", start=eth_etf_flow["Date"][0] |
|
) |
|
eth_price = eth_price.Close |
|
|
|
eth_price = extract_date_index(eth_price) |
|
|
|
|
|
def gen_data(asset): |
|
if asset == "BTC": |
|
etf_volumes = btc_etf_volumes |
|
price = btc_price |
|
|
|
etf_flow_individual = btc_etf_flow.drop(columns="Total") |
|
etf_flow_total = btc_etf_flow[["Date", "Total"]] |
|
else: |
|
etf_volumes = eth_etf_volumes |
|
price = eth_price |
|
|
|
etf_flow_individual = eth_etf_flow.drop(columns="Total") |
|
etf_flow_total = eth_etf_flow[["Date", "Total"]] |
|
|
|
cum_flow_individual = etf_flow_individual.drop(columns="Date").cumsum() |
|
cum_flow_individual["Date"] = etf_flow_individual.Date |
|
cum_flow_total = pd.DataFrame( |
|
{ |
|
"Date": etf_flow_total.Date, |
|
"Total": etf_flow_total.Total.cumsum(), |
|
} |
|
) |
|
|
|
return SimpleNamespace( |
|
etf_volumes=etf_volumes, |
|
price=price, |
|
etf_flow_individual=etf_flow_individual, |
|
etf_flow_total=etf_flow_total, |
|
cum_flow_individual=cum_flow_individual, |
|
cum_flow_total=cum_flow_total, |
|
) |
|
|
|
|
|
def gen_charts(asset, chart_size={"width": 560, "height": 300}): |
|
|
|
data = gen_data(asset) |
|
etf_volumes = data.etf_volumes |
|
price = data.price |
|
etf_flow_individual = data.etf_flow_individual |
|
etf_flow_total = data.etf_flow_total |
|
cum_flow_individual = data.cum_flow_individual |
|
cum_flow_total = data.cum_flow_total |
|
|
|
trading_vol_fig = ( |
|
alt.Chart(etf_volumes) |
|
.transform_fold( |
|
etf_volumes.drop(columns="Date").columns.to_list(), as_=["Funds", "Volume"] |
|
) |
|
.mark_bar() |
|
.encode( |
|
x=alt.X("Date:T", axis=alt.Axis(tickCount="day")), |
|
y=alt.Y("Volume:Q"), |
|
color="Funds:N", |
|
) |
|
) |
|
trading_vol_avg_fig = ( |
|
alt.Chart(etf_volumes) |
|
.transform_fold( |
|
etf_volumes.drop(columns="Date").columns.to_list(), as_=["Funds", "Volume"] |
|
) |
|
.mark_line() |
|
.encode( |
|
x=alt.X("Date:T", axis=alt.Axis(tickCount="day")), |
|
y=alt.Y("mean(Volume):Q", title="Average Volume"), |
|
color=alt.value("crimson"), |
|
) |
|
) |
|
|
|
trading_vol_fig += trading_vol_avg_fig |
|
trading_vol_fig = trading_vol_fig.properties( |
|
title=f"{asset} ETF trading volume", |
|
**chart_size, |
|
) |
|
|
|
|
|
net_flow_individual_fig = ( |
|
alt.Chart(etf_flow_individual) |
|
.transform_fold( |
|
etf_flow_individual.drop(columns="Date").columns.to_list(), |
|
as_=["Funds", "Net Flow"], |
|
) |
|
.mark_bar() |
|
.encode( |
|
x=alt.X("Date:T", axis=alt.Axis(tickCount="day")), |
|
y=alt.Y("Net Flow:Q"), |
|
color="Funds:N", |
|
) |
|
) |
|
net_flow_total_line_fig = ( |
|
alt.Chart(etf_flow_total) |
|
.mark_line() |
|
.encode( |
|
x=alt.X("Date:T", axis=alt.Axis(tickCount="day")), |
|
y=alt.Y("Total:Q"), |
|
color=alt.value("crimson"), |
|
) |
|
) |
|
net_flow_individual_fig += net_flow_total_line_fig |
|
net_flow_individual_fig = net_flow_individual_fig.properties( |
|
title=f"{asset} ETF net flow of individual funds", |
|
**chart_size, |
|
) |
|
|
|
net_flow_total_fig = ( |
|
alt.Chart(etf_flow_total) |
|
.mark_bar() |
|
.encode( |
|
x=alt.X("Date:T", axis=alt.Axis(tickCount="day")), |
|
y=alt.Y("Total:Q"), |
|
color=alt.condition( |
|
alt.datum.Total > 0, |
|
alt.value("seagreen"), |
|
alt.value("orangered"), |
|
), |
|
) |
|
) |
|
|
|
price_fig = ( |
|
alt.Chart(price) |
|
.mark_line() |
|
.encode( |
|
x=alt.X("Date:T", axis=alt.Axis(tickCount="day")), |
|
y=alt.Y("Close:Q", title="Price"), |
|
color=alt.value("crimson"), |
|
) |
|
) |
|
|
|
net_flow_total_fig += price_fig |
|
net_flow_total_fig = net_flow_total_fig.resolve_scale( |
|
y="independent", |
|
).properties( |
|
title=f"{asset} ETF net flow total vs asset price", |
|
**chart_size, |
|
) |
|
|
|
|
|
cum_flow_individual_net_fig = ( |
|
alt.Chart(cum_flow_individual) |
|
.transform_fold( |
|
cum_flow_individual.drop(columns="Date").columns.to_list(), |
|
as_=["Funds", "Net Flow"], |
|
) |
|
.mark_area() |
|
.encode( |
|
x=alt.X("Date:T", axis=alt.Axis(tickCount="day")), |
|
y=alt.Y("Net Flow:Q"), |
|
color=alt.Color("Funds:N", scale=alt.Scale(scheme="tableau20")), |
|
) |
|
) |
|
cum_flow_individual_net_fig += price_fig |
|
cum_flow_individual_net_fig = cum_flow_individual_net_fig.resolve_scale( |
|
y="independent", |
|
).properties( |
|
title=f"{asset} ETF cumulative flow of individual funds vs asset price", |
|
**chart_size, |
|
) |
|
|
|
|
|
cum_flow_total_fig = ( |
|
alt.Chart(cum_flow_total) |
|
.transform_calculate( |
|
negative="datum.Total < 0", |
|
) |
|
.mark_area() |
|
.encode( |
|
x=alt.X("Date:T", axis=alt.Axis(tickCount="day")), |
|
y=alt.Y("Total:Q", impute={"value": 0}), |
|
color=alt.Color( |
|
"negative:N", title="Negative Flow", scale=alt.Scale(scheme="set2") |
|
), |
|
) |
|
) |
|
cum_flow_total_fig += price_fig |
|
cum_flow_total_fig = cum_flow_total_fig.resolve_scale( |
|
y="independent", |
|
).properties( |
|
title=f"{asset} ETF cumulative flow total vs asset price", |
|
**chart_size, |
|
) |
|
|
|
return SimpleNamespace( |
|
trading_vol_fig=trading_vol_fig, |
|
net_flow_individual_fig=net_flow_individual_fig, |
|
net_flow_total_fig=net_flow_total_fig, |
|
cum_flow_individual_net_fig=cum_flow_individual_net_fig, |
|
cum_flow_total_fig=cum_flow_total_fig, |
|
) |
|
|
|
|
|
def compound_chart(chart_size={"width": 560, "height": 300}): |
|
btc_charts = gen_charts("BTC", chart_size) |
|
eth_charts = gen_charts("ETH", chart_size) |
|
|
|
|
|
all_charts_btc = ( |
|
btc_charts.trading_vol_fig |
|
& btc_charts.net_flow_individual_fig |
|
& btc_charts.net_flow_total_fig |
|
& btc_charts.cum_flow_individual_net_fig |
|
& btc_charts.cum_flow_total_fig |
|
).resolve_scale(color="independent") |
|
all_charts_eth = ( |
|
eth_charts.trading_vol_fig |
|
& eth_charts.net_flow_individual_fig |
|
& eth_charts.net_flow_total_fig |
|
& eth_charts.cum_flow_individual_net_fig |
|
& eth_charts.cum_flow_total_fig |
|
).resolve_scale(color="independent") |
|
|
|
all_charts = (all_charts_btc | all_charts_eth).resolve_scale(color="independent") |
|
|
|
return all_charts |
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
st.set_page_config(layout="wide", page_icon="π") |
|
|
|
chart = compound_chart(chart_size={"width": 560, "height": 300}) |
|
|
|
st.altair_chart(chart, use_container_width=True) |
|
|