InNoobWeTrust
commited on
Commit
β’
97ae727
1
Parent(s):
4ab50ad
feat: dashboard for BTC/ETH
Browse files- .devcontainer/devcontainer.json +3 -0
- requirements.txt +5 -1
- streamlit_app.py +248 -3
.devcontainer/devcontainer.json
CHANGED
@@ -27,6 +27,9 @@
|
|
27 |
"onAutoForward": "openPreview"
|
28 |
}
|
29 |
},
|
|
|
|
|
|
|
30 |
"forwardPorts": [
|
31 |
8501
|
32 |
]
|
|
|
27 |
"onAutoForward": "openPreview"
|
28 |
}
|
29 |
},
|
30 |
+
"appPort": [
|
31 |
+
8501
|
32 |
+
],
|
33 |
"forwardPorts": [
|
34 |
8501
|
35 |
]
|
requirements.txt
CHANGED
@@ -1 +1,5 @@
|
|
1 |
-
streamlit
|
|
|
|
|
|
|
|
|
|
1 |
+
streamlit
|
2 |
+
numpy
|
3 |
+
pandas
|
4 |
+
yfinance
|
5 |
+
plotly
|
streamlit_app.py
CHANGED
@@ -1,6 +1,251 @@
|
|
1 |
import streamlit as st
|
2 |
|
3 |
-
|
4 |
-
|
5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import streamlit as st
|
2 |
|
3 |
+
import numpy as np
|
4 |
+
import pandas as pd
|
5 |
+
import yfinance as yf
|
6 |
+
|
7 |
+
import plotly.express as px
|
8 |
+
import plotly.graph_objects as go
|
9 |
+
from plotly.subplots import make_subplots
|
10 |
+
|
11 |
+
|
12 |
+
def clean_etf_data(df):
|
13 |
+
"""
|
14 |
+
Clean ETF data
|
15 |
+
"""
|
16 |
+
# Set date as index
|
17 |
+
df.Date = pd.to_datetime(df.Date, dayfirst=True)
|
18 |
+
df.set_index("Date", inplace=True)
|
19 |
+
# Format index to date without time
|
20 |
+
df.index = df.index.normalize().date
|
21 |
+
# Format outflow to negative value
|
22 |
+
df.replace(to_replace=r"\(([0-9.]+)\)", value=r"-\1", regex=True, inplace=True)
|
23 |
+
|
24 |
+
# Copy original
|
25 |
+
df_original = df.copy()
|
26 |
+
|
27 |
+
# Replace '-' with 0
|
28 |
+
df.replace("-", 0, inplace=True)
|
29 |
+
|
30 |
+
# Convert from strings to numberic
|
31 |
+
df = df.apply(pd.to_numeric)
|
32 |
+
|
33 |
+
return df, df_original
|
34 |
+
|
35 |
+
|
36 |
+
##------------------------- ETF flow --------------------------------------------
|
37 |
+
|
38 |
+
# Get Bitcoin spot ETF history
|
39 |
+
btc_etf_flow = pd.read_html(
|
40 |
+
"https://farside.co.uk/?p=1321", attrs={"class": "etf"}, skiprows=[1]
|
41 |
+
)[0]
|
42 |
+
# Drop column 'BTC'
|
43 |
+
# btc_etf_flow.drop(columns = ['BTC'], inplace = True)
|
44 |
+
# Remove summary lines
|
45 |
+
btc_etf_flow = btc_etf_flow.iloc[:-4]
|
46 |
+
# Extract symbols of ETF funds
|
47 |
+
btc_etf_funds = btc_etf_flow.drop(["Date", "Total"], axis=1).columns
|
48 |
+
|
49 |
+
# Get Ethereum spot ETF history
|
50 |
+
eth_etf_flow = pd.read_html(
|
51 |
+
"https://farside.co.uk/ethereum-etf-flow-all-data/",
|
52 |
+
attrs={"class": "etf"},
|
53 |
+
skiprows=[2, 3],
|
54 |
+
)[0]
|
55 |
+
# Drop column index level 2
|
56 |
+
eth_etf_flow.columns = eth_etf_flow.columns.droplevel(2)
|
57 |
+
# Extract symbols of ETF funds
|
58 |
+
eth_etf_funds = eth_etf_flow.drop("Total", axis=1).columns[1:].get_level_values(1)
|
59 |
+
# Merge multi-index columns
|
60 |
+
eth_etf_flow.columns = eth_etf_flow.columns.map(" | ".join)
|
61 |
+
# Name first column "Date"
|
62 |
+
eth_etf_flow.rename(
|
63 |
+
columns={
|
64 |
+
"Unnamed: 0_level_0 | Unnamed: 0_level_1": "Date",
|
65 |
+
"Total | Unnamed: 10_level_1": "Total",
|
66 |
+
},
|
67 |
+
inplace=True,
|
68 |
+
)
|
69 |
+
# Remove summary lines
|
70 |
+
eth_etf_flow = eth_etf_flow.iloc[:-1]
|
71 |
+
|
72 |
+
btc_etf_flow, btc_etf_flow_original = clean_etf_data(btc_etf_flow)
|
73 |
+
eth_etf_flow, eth_etf_flow_original = clean_etf_data(eth_etf_flow)
|
74 |
+
|
75 |
+
##------------------------- ETF volume -----------------------------------------
|
76 |
+
|
77 |
+
# Get BTC ETF daily volume
|
78 |
+
btc_etf_volumes = pd.DataFrame()
|
79 |
+
for fund in btc_etf_funds:
|
80 |
+
btc_etf_volumes[fund] = yf.download(
|
81 |
+
f"{fund}", interval="1d", period="max", start=btc_etf_flow.index[0]
|
82 |
+
)["Volume"]
|
83 |
+
|
84 |
+
# Format index to date without time
|
85 |
+
btc_etf_volumes.index = btc_etf_volumes.index.normalize().date
|
86 |
+
|
87 |
+
# Get ETH ETF daily volume
|
88 |
+
eth_etf_volumes = pd.DataFrame()
|
89 |
+
for fund in eth_etf_funds:
|
90 |
+
eth_etf_volumes[fund] = yf.download(
|
91 |
+
f"{fund}", interval="1d", period="max", start=eth_etf_flow.index[0]
|
92 |
+
)["Volume"]
|
93 |
+
|
94 |
+
# Format index to date without time
|
95 |
+
eth_etf_volumes.index = eth_etf_volumes.index.normalize().date
|
96 |
+
|
97 |
+
##------------------------- Asset price --------------------------------------------
|
98 |
+
|
99 |
+
# Get BTC price history
|
100 |
+
btc_price = yf.download(
|
101 |
+
"BTC-USD", interval="1d", period="max", start=btc_etf_flow.index[0]
|
102 |
)
|
103 |
+
btc_price = btc_price.Close
|
104 |
+
# Format index to date without time
|
105 |
+
btc_price.index = btc_price.index.normalize().date
|
106 |
+
|
107 |
+
# Get ETH price history
|
108 |
+
eth_price = yf.download(
|
109 |
+
"ETH-USD", interval="1d", period="max", start=eth_etf_flow.index[0]
|
110 |
+
)
|
111 |
+
eth_price = eth_price.Close
|
112 |
+
# Format index to date without time
|
113 |
+
eth_price.index = eth_price.index.normalize().date
|
114 |
+
|
115 |
+
|
116 |
+
if __name__ == "__main__":
|
117 |
+
# Set page config
|
118 |
+
st.set_page_config(layout="wide", page_icon="π")
|
119 |
+
# Set page title
|
120 |
+
st.title("Crypto ETF Dashboard")
|
121 |
+
|
122 |
+
# Sidebar to choose ETF asset to view
|
123 |
+
st.sidebar.title("Crypto ETF Dashboard")
|
124 |
+
# Dropdown selection to choose asset (BTC, ETH)
|
125 |
+
asset = st.sidebar.selectbox("Choose asset", ("BTC", "ETH"))
|
126 |
+
|
127 |
+
# Display ETF data
|
128 |
+
if asset == "BTC":
|
129 |
+
st.header("BTC ETF")
|
130 |
+
etf_flow = btc_etf_flow
|
131 |
+
etf_volumes = btc_etf_volumes
|
132 |
+
price = btc_price
|
133 |
+
else:
|
134 |
+
st.header("ETH ETF")
|
135 |
+
etf_flow = eth_etf_flow
|
136 |
+
etf_volumes = eth_etf_volumes
|
137 |
+
price = eth_price
|
138 |
+
|
139 |
+
etf_flow_individual = etf_flow.drop("Total", axis=1)
|
140 |
+
etf_flow_total = etf_flow.Total
|
141 |
+
|
142 |
+
# Section trading volume
|
143 |
+
st.subheader(f"{asset} ETF Trading volume")
|
144 |
+
trading_vol_fig = px.bar(
|
145 |
+
etf_volumes, x=etf_volumes.index, y=etf_volumes.columns, barmode="relative"
|
146 |
+
)
|
147 |
+
st.plotly_chart(trading_vol_fig, use_container_width=True)
|
148 |
+
|
149 |
+
# Section net flow individual funds
|
150 |
+
st.subheader(f"{asset} ETF Net flow individual funds")
|
151 |
+
net_flow_individual_fig = px.bar(
|
152 |
+
etf_flow_individual,
|
153 |
+
x=etf_flow_individual.index,
|
154 |
+
y=etf_flow_individual.columns,
|
155 |
+
barmode="relative",
|
156 |
+
)
|
157 |
+
st.plotly_chart(net_flow_individual_fig, use_container_width=True)
|
158 |
+
|
159 |
+
# Section net flow total vs asset price
|
160 |
+
st.subheader(f"{asset} ETF Net flow total vs asset price")
|
161 |
+
positive_flow = etf_flow_total[etf_flow_total > 0]
|
162 |
+
negative_flow = etf_flow_total[etf_flow_total < 0]
|
163 |
+
net_flow_total_fig = make_subplots(specs=[[{"secondary_y": True}]])
|
164 |
+
# Sea green bar for positive flow
|
165 |
+
net_flow_total_fig.add_trace(
|
166 |
+
go.Bar(
|
167 |
+
x=positive_flow.index,
|
168 |
+
y=positive_flow,
|
169 |
+
name="Total (positive)",
|
170 |
+
marker_color="seagreen",
|
171 |
+
),
|
172 |
+
secondary_y=False,
|
173 |
+
)
|
174 |
+
# Orange red bar for negative flow
|
175 |
+
net_flow_total_fig.add_trace(
|
176 |
+
go.Bar(
|
177 |
+
x=negative_flow.index,
|
178 |
+
y=negative_flow,
|
179 |
+
name="Total (negative)",
|
180 |
+
marker_color="orangered",
|
181 |
+
),
|
182 |
+
secondary_y=False,
|
183 |
+
)
|
184 |
+
# Line chart of price
|
185 |
+
net_flow_total_fig.add_trace(
|
186 |
+
go.Scatter(
|
187 |
+
x=price.index,
|
188 |
+
y=price,
|
189 |
+
name=f"{asset} Price",
|
190 |
+
mode="lines",
|
191 |
+
line=dict(color="darkgoldenrod"),
|
192 |
+
),
|
193 |
+
secondary_y=True,
|
194 |
+
)
|
195 |
+
net_flow_total_fig.update_layout(barmode="stack")
|
196 |
+
st.plotly_chart(net_flow_total_fig, use_container_width=True)
|
197 |
+
|
198 |
+
# Section cumulative flow individual vs asset price
|
199 |
+
st.subheader(f"{asset} ETF Cumulative flow of individual funds vs asset price")
|
200 |
+
cum_flow_individual = etf_flow_individual.cumsum()
|
201 |
+
cum_flow_individual_fig = make_subplots(specs=[[{"secondary_y": True}]])
|
202 |
+
# Stacking area chart of flow from individual funds
|
203 |
+
for col in cum_flow_individual.columns:
|
204 |
+
cum_flow_individual_fig.add_trace(
|
205 |
+
go.Scatter(
|
206 |
+
x=cum_flow_individual.index,
|
207 |
+
y=cum_flow_individual[col],
|
208 |
+
name=col,
|
209 |
+
fill="tonexty",
|
210 |
+
),
|
211 |
+
secondary_y=False,
|
212 |
+
)
|
213 |
+
# Line chart of price
|
214 |
+
cum_flow_individual_fig.add_trace(
|
215 |
+
go.Scatter(
|
216 |
+
x=price.index,
|
217 |
+
y=price,
|
218 |
+
name=f"{asset} Price",
|
219 |
+
mode="lines",
|
220 |
+
line=dict(color="darkgoldenrod"),
|
221 |
+
),
|
222 |
+
secondary_y=True,
|
223 |
+
)
|
224 |
+
st.plotly_chart(cum_flow_individual_fig, use_container_width=True)
|
225 |
+
|
226 |
+
# Section cumulative flow total vs asset price
|
227 |
+
st.subheader(f"{asset} ETF Cumulative flow total vs asset price")
|
228 |
+
cum_flow_total = etf_flow_total.cumsum()
|
229 |
+
cum_flow_total_fig = make_subplots(specs=[[{"secondary_y": True}]])
|
230 |
+
# Area chart for cumulative flow
|
231 |
+
cum_flow_total_fig.add_trace(
|
232 |
+
go.Scatter(
|
233 |
+
x=cum_flow_total.index,
|
234 |
+
y=cum_flow_total,
|
235 |
+
name="Cumulative flow total",
|
236 |
+
fill="tonexty",
|
237 |
+
),
|
238 |
+
secondary_y=False,
|
239 |
+
)
|
240 |
+
# Line chart of price
|
241 |
+
cum_flow_total_fig.add_trace(
|
242 |
+
go.Scatter(
|
243 |
+
x=price.index,
|
244 |
+
y=price,
|
245 |
+
name=f"{asset} Price",
|
246 |
+
mode="lines",
|
247 |
+
line=dict(color="darkgoldenrod"),
|
248 |
+
),
|
249 |
+
secondary_y=True,
|
250 |
+
)
|
251 |
+
st.plotly_chart(cum_flow_total_fig, use_container_width=True)
|