stellar-stats 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stellar_stats/__init__.py +0 -0
- stellar_stats/app.py +646 -0
- stellar_stats/auth.py +35 -0
- stellar_stats/cli.py +94 -0
- stellar_stats/config.py +62 -0
- stellar_stats/data.py +298 -0
- stellar_stats/roundtrip.py +238 -0
- stellar_stats/stats.py +208 -0
- stellar_stats/ui.py +500 -0
- stellar_stats/utils.py +111 -0
- stellar_stats-0.8.0.dist-info/METADATA +76 -0
- stellar_stats-0.8.0.dist-info/RECORD +15 -0
- stellar_stats-0.8.0.dist-info/WHEEL +5 -0
- stellar_stats-0.8.0.dist-info/entry_points.txt +2 -0
- stellar_stats-0.8.0.dist-info/top_level.txt +1 -0
|
File without changes
|
stellar_stats/app.py
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import empyrical as ep
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import streamlit as st
|
|
8
|
+
from tabulate import tabulate
|
|
9
|
+
|
|
10
|
+
from .auth import setup_authentication
|
|
11
|
+
from .config import load_config, sort_accounts_by_mtime
|
|
12
|
+
from .data import (
|
|
13
|
+
calculate_max_principle_series,
|
|
14
|
+
load_benchmark,
|
|
15
|
+
load_investor_returns,
|
|
16
|
+
load_returns,
|
|
17
|
+
load_round_trips,
|
|
18
|
+
load_slippage,
|
|
19
|
+
load_trades,
|
|
20
|
+
)
|
|
21
|
+
from .stats import (
|
|
22
|
+
adjust_rebate,
|
|
23
|
+
gen_drawdown_table,
|
|
24
|
+
gen_perf,
|
|
25
|
+
)
|
|
26
|
+
from .ui import (
|
|
27
|
+
plot_cumulative_returns,
|
|
28
|
+
plot_monthly_returns,
|
|
29
|
+
plot_monthly_returns_heatmap,
|
|
30
|
+
plot_profit_distribution,
|
|
31
|
+
plot_return_distribution,
|
|
32
|
+
plot_slippage_distribution,
|
|
33
|
+
plot_underwater,
|
|
34
|
+
plot_weekly_returns,
|
|
35
|
+
plot_yearly_returns,
|
|
36
|
+
show_performance_metrics,
|
|
37
|
+
show_trade_metrics,
|
|
38
|
+
)
|
|
39
|
+
from .utils import refresh_cache, show_col_desc
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def app():
|
|
43
|
+
st.set_page_config(
|
|
44
|
+
page_title="Trade Stats Dashboard",
|
|
45
|
+
page_icon="📈",
|
|
46
|
+
layout="wide",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Load configuration and setup
|
|
50
|
+
cfg, pro, accounts, datadirs, index_funcs = load_config()
|
|
51
|
+
|
|
52
|
+
# Setup authentication
|
|
53
|
+
setup_authentication(cfg)
|
|
54
|
+
|
|
55
|
+
# Refresh cache
|
|
56
|
+
refresh_cache(datadirs)
|
|
57
|
+
|
|
58
|
+
# Sort accounts by modification time
|
|
59
|
+
accounts = sort_accounts_by_mtime(accounts, datadirs)
|
|
60
|
+
|
|
61
|
+
# Setup benchmark accounts
|
|
62
|
+
benchmark_accounts = list(index_funcs.keys()) + accounts
|
|
63
|
+
benchmark_accounts.append("Custom Symbol")
|
|
64
|
+
benchmark_idx = len(index_funcs) + 1
|
|
65
|
+
|
|
66
|
+
# Initialize session state
|
|
67
|
+
if "authenticated" not in st.session_state:
|
|
68
|
+
st.session_state.authenticated = False
|
|
69
|
+
|
|
70
|
+
# Account selection
|
|
71
|
+
def check_account_widget(accounts):
|
|
72
|
+
if "account" not in st.session_state:
|
|
73
|
+
return False
|
|
74
|
+
return all(elem in accounts for elem in st.session_state.account)
|
|
75
|
+
|
|
76
|
+
def check_benchmark_widget(benchmark_accounts):
|
|
77
|
+
if "benchmark" not in st.session_state:
|
|
78
|
+
return False
|
|
79
|
+
return st.session_state.benchmark in benchmark_accounts
|
|
80
|
+
|
|
81
|
+
if st.session_state.get("swap_once"):
|
|
82
|
+
account = st.session_state.benchmark
|
|
83
|
+
if len(st.session_state.account) > 0:
|
|
84
|
+
benchmark = st.session_state.account[0]
|
|
85
|
+
else:
|
|
86
|
+
benchmark = accounts[1]
|
|
87
|
+
st.session_state.account = [account]
|
|
88
|
+
st.session_state.benchmark = benchmark
|
|
89
|
+
st.session_state["swap_once"] = False
|
|
90
|
+
else:
|
|
91
|
+
st.session_state.account = st.session_state.get("account", [accounts[0]])
|
|
92
|
+
st.session_state.benchmark = st.session_state.get(
|
|
93
|
+
"benchmark", benchmark_accounts[benchmark_idx]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if not check_account_widget(accounts):
|
|
97
|
+
st.warning("Account not found, reset to default account")
|
|
98
|
+
st.session_state.account = [accounts[0]]
|
|
99
|
+
|
|
100
|
+
selected_accounts = st.sidebar.multiselect(
|
|
101
|
+
"Select Account", accounts, key="account"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if len(selected_accounts) > 1:
|
|
105
|
+
account = "Combined"
|
|
106
|
+
elif len(selected_accounts) == 1:
|
|
107
|
+
account = selected_accounts[0]
|
|
108
|
+
else:
|
|
109
|
+
selected_accounts = [accounts[0]]
|
|
110
|
+
account = selected_accounts[0]
|
|
111
|
+
|
|
112
|
+
def swap_account_benchmark():
|
|
113
|
+
st.session_state["swap_once"] = True
|
|
114
|
+
|
|
115
|
+
st.sidebar.button(" ↕ ", on_click=swap_account_benchmark)
|
|
116
|
+
|
|
117
|
+
if not check_benchmark_widget(benchmark_accounts):
|
|
118
|
+
st.warning("Benchmark not found, reset to default benchmark")
|
|
119
|
+
st.session_state.benchmark = benchmark_accounts[benchmark_idx]
|
|
120
|
+
|
|
121
|
+
benchmark = st.sidebar.selectbox(
|
|
122
|
+
"Select Benchmark", benchmark_accounts, key="benchmark"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if benchmark == "Custom Symbol":
|
|
126
|
+
benchmark = st.sidebar.text_input("Benchmark Symbol", "MSFT")
|
|
127
|
+
|
|
128
|
+
bm_ratio = st.sidebar.selectbox("Set Benchmark Leverage", [1, 2, 3, 4, 5], 0)
|
|
129
|
+
|
|
130
|
+
if st.session_state.get("swap_once"):
|
|
131
|
+
st.rerun()
|
|
132
|
+
|
|
133
|
+
# Load investors data
|
|
134
|
+
if len(selected_accounts) > 1:
|
|
135
|
+
investors = None
|
|
136
|
+
else:
|
|
137
|
+
if os.path.exists("investors.csv"):
|
|
138
|
+
investors = pd.read_csv("investors.csv")
|
|
139
|
+
investors["date"] = pd.to_datetime(investors["date"])
|
|
140
|
+
investors = investors.sort_values("date")
|
|
141
|
+
investors = investors[investors["account"].isin(selected_accounts)]
|
|
142
|
+
if len(investors) == 0:
|
|
143
|
+
investors = None
|
|
144
|
+
else:
|
|
145
|
+
investors = None
|
|
146
|
+
|
|
147
|
+
st.header("%s Performance Stats" % (account if cfg is not None else "Strategy"))
|
|
148
|
+
st.markdown("""---""")
|
|
149
|
+
|
|
150
|
+
# Load returns and benchmark data
|
|
151
|
+
raw_returns = load_returns(selected_accounts, datadirs)
|
|
152
|
+
rebate_threshold = 0.01 / len(selected_accounts)
|
|
153
|
+
returns = adjust_rebate(raw_returns, rebate_threshold)
|
|
154
|
+
# max_principle = calculate_max_principle_series(returns)
|
|
155
|
+
bm_returns = load_benchmark(benchmark, index_funcs, datadirs)
|
|
156
|
+
|
|
157
|
+
if bm_returns is not None:
|
|
158
|
+
bm_returns["returns"] *= bm_ratio
|
|
159
|
+
|
|
160
|
+
# Load trade data
|
|
161
|
+
trades = load_trades(selected_accounts, datadirs)
|
|
162
|
+
rts = load_round_trips(selected_accounts, datadirs)
|
|
163
|
+
slippage = load_slippage(selected_accounts, datadirs)
|
|
164
|
+
|
|
165
|
+
# Load investor returns
|
|
166
|
+
investor_returns = load_investor_returns(investors, returns)
|
|
167
|
+
|
|
168
|
+
# Period selection
|
|
169
|
+
years = list(returns.index.year.unique())
|
|
170
|
+
years.sort(reverse=True)
|
|
171
|
+
year = years[0]
|
|
172
|
+
|
|
173
|
+
periods = [
|
|
174
|
+
"Year To Date",
|
|
175
|
+
"Since Inception",
|
|
176
|
+
"Custom Year Range",
|
|
177
|
+
"Custom Date Range",
|
|
178
|
+
]
|
|
179
|
+
with st.sidebar:
|
|
180
|
+
period = st.selectbox("Select Period", periods, 1 if cfg is None else 0)
|
|
181
|
+
|
|
182
|
+
if period == "Year To Date":
|
|
183
|
+
returns = adjust_rebate(returns[returns.index.year == year])
|
|
184
|
+
elif period == "Since Inception":
|
|
185
|
+
returns = adjust_rebate(returns)
|
|
186
|
+
elif period == "Custom Year Range":
|
|
187
|
+
start_year = st.text_input("Start Year", returns.index[-1].date().year)
|
|
188
|
+
end_year = st.text_input("End Year", returns.index[-1].date().year)
|
|
189
|
+
if start_year > end_year:
|
|
190
|
+
st.error("Error: end year should not be earlier than start year.")
|
|
191
|
+
else:
|
|
192
|
+
returns = adjust_rebate(returns[start_year:end_year])
|
|
193
|
+
elif period == "Custom Date Range":
|
|
194
|
+
start_date = st.date_input("Start Date", datetime.date(year, 1, 2))
|
|
195
|
+
end_date = st.date_input("End Date", returns.index[-1].date())
|
|
196
|
+
if start_date > end_date:
|
|
197
|
+
st.error("Error: end date should not be earlier than start date.")
|
|
198
|
+
else:
|
|
199
|
+
returns = adjust_rebate(
|
|
200
|
+
returns[
|
|
201
|
+
(returns.index.date >= start_date)
|
|
202
|
+
& (returns.index.date <= end_date)
|
|
203
|
+
]
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
returns = adjust_rebate(returns[returns.index.year == period])
|
|
207
|
+
|
|
208
|
+
start_date = returns.index.date[0]
|
|
209
|
+
end_date = returns.index.date[-1]
|
|
210
|
+
|
|
211
|
+
# max_principle = max_principle[
|
|
212
|
+
# (max_principle.index.date >= start_date) & (max_principle.index.date <= end_date)
|
|
213
|
+
# ]
|
|
214
|
+
|
|
215
|
+
if bm_returns is not None:
|
|
216
|
+
bm_returns = adjust_rebate(
|
|
217
|
+
bm_returns[
|
|
218
|
+
(bm_returns.index.date >= start_date)
|
|
219
|
+
& (bm_returns.index.date <= end_date)
|
|
220
|
+
]
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# # Recalculate investor returns for the selected period
|
|
224
|
+
# for investor, ireturns in investor_returns.items():
|
|
225
|
+
# filtered_returns = ireturns[
|
|
226
|
+
# (ireturns.index.date >= start_date) & (ireturns.index.date <= end_date)
|
|
227
|
+
# ]
|
|
228
|
+
# # Only process if there's actual data in the filtered range
|
|
229
|
+
# if not filtered_returns.empty:
|
|
230
|
+
# ireturns = return_stats(filtered_returns)
|
|
231
|
+
# if ireturns is not None:
|
|
232
|
+
# investor_returns[investor] = ireturns.sort_index(ascending=False)
|
|
233
|
+
# else:
|
|
234
|
+
# # Remove investors with no data in the selected range
|
|
235
|
+
# investor_returns[investor] = None
|
|
236
|
+
|
|
237
|
+
# Filter trade data by date range
|
|
238
|
+
if trades is not None:
|
|
239
|
+
if "date" in trades.columns:
|
|
240
|
+
trades = trades.set_index("date")
|
|
241
|
+
elif "datetime" in trades.columns:
|
|
242
|
+
trades = trades.set_index("datetime")
|
|
243
|
+
elif "timestamp" in trades.columns:
|
|
244
|
+
trades["timestamp"] = trades["timestamp"].apply(
|
|
245
|
+
lambda x: pd.Timestamp(x).tz_localize(None)
|
|
246
|
+
)
|
|
247
|
+
trades = trades.set_index("timestamp")
|
|
248
|
+
|
|
249
|
+
if trades is not None:
|
|
250
|
+
trades = trades[
|
|
251
|
+
(trades.index.date >= start_date) & (trades.index.date <= end_date)
|
|
252
|
+
]
|
|
253
|
+
if rts is not None:
|
|
254
|
+
rts = rts[(rts.index.date >= start_date) & (rts.index.date <= end_date)]
|
|
255
|
+
if slippage is not None:
|
|
256
|
+
slippage = slippage[
|
|
257
|
+
(slippage.index.date >= start_date) & (slippage.index.date <= end_date)
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
if trades is not None and len(trades) == 0:
|
|
261
|
+
trades = None
|
|
262
|
+
if rts is not None and len(rts) == 0:
|
|
263
|
+
rts = None
|
|
264
|
+
if slippage is not None and len(slippage) == 0:
|
|
265
|
+
slippage = None
|
|
266
|
+
|
|
267
|
+
if bm_returns is None:
|
|
268
|
+
bm_returns = returns.copy()
|
|
269
|
+
bm_returns.loc[:, :] = 0.0
|
|
270
|
+
|
|
271
|
+
returns["benchmark_cum_returns"] = bm_returns["cum_returns"]
|
|
272
|
+
returns["benchmark_underwater"] = bm_returns["underwater"]
|
|
273
|
+
|
|
274
|
+
# Generate drawdown tables
|
|
275
|
+
drawdowns = gen_drawdown_table(returns["returns"], 10)
|
|
276
|
+
nan_rows = drawdowns["Duration"].isna()
|
|
277
|
+
drawdowns.loc[nan_rows, "Duration"] = (
|
|
278
|
+
returns.index[-1] - drawdowns.loc[nan_rows, "Peak date"]
|
|
279
|
+
).dt.days
|
|
280
|
+
drawdowns["Peak date"] = drawdowns["Peak date"].dt.date
|
|
281
|
+
drawdowns["Valley date"] = drawdowns["Valley date"].dt.date
|
|
282
|
+
drawdowns["Recovery date"] = drawdowns["Recovery date"].dt.date
|
|
283
|
+
drawdowns["Net drawdown in %"] *= -1
|
|
284
|
+
drawdowns = drawdowns.rename(columns={"Net drawdown in %": "Drawdown %"})
|
|
285
|
+
drawdowns = drawdowns.dropna(axis=0, how="all")
|
|
286
|
+
|
|
287
|
+
bm_drawdowns = gen_drawdown_table(bm_returns["returns"], 10)
|
|
288
|
+
bm_nan_rows = bm_drawdowns["Duration"].isna()
|
|
289
|
+
bm_drawdowns.loc[bm_nan_rows, "Duration"] = (
|
|
290
|
+
bm_returns.index[-1] - bm_drawdowns.loc[nan_rows, "Peak date"]
|
|
291
|
+
).dt.days
|
|
292
|
+
bm_drawdowns["Peak date"] = bm_drawdowns["Peak date"].dt.date
|
|
293
|
+
bm_drawdowns["Valley date"] = bm_drawdowns["Valley date"].dt.date
|
|
294
|
+
bm_drawdowns["Recovery date"] = bm_drawdowns["Recovery date"].dt.date
|
|
295
|
+
bm_drawdowns["Net drawdown in %"] *= -1
|
|
296
|
+
bm_drawdowns = bm_drawdowns.rename(columns={"Net drawdown in %": "Drawdown %"})
|
|
297
|
+
|
|
298
|
+
# Generate return tables
|
|
299
|
+
yearly_return = (
|
|
300
|
+
returns.groupby(pd.Grouper(freq="YE"))
|
|
301
|
+
.apply(gen_perf)
|
|
302
|
+
.sort_index(ascending=True)
|
|
303
|
+
.dropna()
|
|
304
|
+
)
|
|
305
|
+
yearly_return.insert(0, "Year", yearly_return.index.year.astype(str))
|
|
306
|
+
yearly_return = yearly_return.reset_index().drop("date", axis=1).set_index("Year")
|
|
307
|
+
|
|
308
|
+
bm_yearly_return = (
|
|
309
|
+
bm_returns.groupby(pd.Grouper(freq="YE"))
|
|
310
|
+
.apply(gen_perf)
|
|
311
|
+
.sort_index(ascending=True)
|
|
312
|
+
.dropna()
|
|
313
|
+
)
|
|
314
|
+
bm_yearly_return.insert(0, "Year", bm_yearly_return.index.year.astype(str))
|
|
315
|
+
bm_yearly_return = (
|
|
316
|
+
bm_yearly_return.reset_index().drop("date", axis=1).set_index("Year")
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
monthly_return = (
|
|
320
|
+
returns.groupby(pd.Grouper(freq="ME"))
|
|
321
|
+
.apply(gen_perf)
|
|
322
|
+
.sort_index(ascending=False)
|
|
323
|
+
.dropna()
|
|
324
|
+
)
|
|
325
|
+
monthly_return.insert(
|
|
326
|
+
0,
|
|
327
|
+
"Timespan",
|
|
328
|
+
monthly_return.index.year.astype(str)
|
|
329
|
+
+ "-"
|
|
330
|
+
+ monthly_return.index.month.map("{:02}".format),
|
|
331
|
+
)
|
|
332
|
+
monthly_return = (
|
|
333
|
+
monthly_return.reset_index().drop("date", axis=1).set_index("Timespan")
|
|
334
|
+
)
|
|
335
|
+
monthly_return = monthly_return.sort_index()
|
|
336
|
+
|
|
337
|
+
bm_monthly_return = (
|
|
338
|
+
bm_returns.groupby(pd.Grouper(freq="ME"))
|
|
339
|
+
.apply(gen_perf)
|
|
340
|
+
.sort_index(ascending=False)
|
|
341
|
+
.dropna()
|
|
342
|
+
)
|
|
343
|
+
bm_monthly_return.insert(
|
|
344
|
+
0,
|
|
345
|
+
"Timespan",
|
|
346
|
+
bm_monthly_return.index.year.astype(str)
|
|
347
|
+
+ "-"
|
|
348
|
+
+ bm_monthly_return.index.month.map("{:02}".format),
|
|
349
|
+
)
|
|
350
|
+
bm_monthly_return = (
|
|
351
|
+
bm_monthly_return.reset_index()
|
|
352
|
+
.drop("date", axis=1)
|
|
353
|
+
.set_index("Timespan")
|
|
354
|
+
.sort_index()
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
weekly_return = (
|
|
358
|
+
returns.groupby(pd.Grouper(freq="W"))
|
|
359
|
+
.apply(gen_perf)
|
|
360
|
+
.sort_index(ascending=False)
|
|
361
|
+
.dropna()
|
|
362
|
+
)
|
|
363
|
+
weekly_return.insert(
|
|
364
|
+
0,
|
|
365
|
+
"Timespan",
|
|
366
|
+
weekly_return.index.isocalendar().year.astype(str)
|
|
367
|
+
+ " Week "
|
|
368
|
+
+ weekly_return.index.isocalendar().week.astype(str),
|
|
369
|
+
)
|
|
370
|
+
weekly_return = (
|
|
371
|
+
weekly_return.reset_index().drop("date", axis=1).set_index("Timespan")
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
bm_weekly_return = (
|
|
375
|
+
bm_returns.groupby(pd.Grouper(freq="W"))
|
|
376
|
+
.apply(gen_perf)
|
|
377
|
+
.sort_index(ascending=False)
|
|
378
|
+
.dropna()
|
|
379
|
+
)
|
|
380
|
+
bm_weekly_return.insert(
|
|
381
|
+
0,
|
|
382
|
+
"Timespan",
|
|
383
|
+
bm_weekly_return.index.isocalendar().year.astype(str)
|
|
384
|
+
+ " Week "
|
|
385
|
+
+ bm_weekly_return.index.isocalendar().week.astype(str),
|
|
386
|
+
)
|
|
387
|
+
bm_weekly_return = (
|
|
388
|
+
bm_weekly_return.reset_index().drop("date", axis=1).set_index("Timespan")
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Calculate underlying breakdowns
|
|
392
|
+
ul_breakdown = None
|
|
393
|
+
slippage_breakdown = None
|
|
394
|
+
if trades is not None:
|
|
395
|
+
turnover_col_name = "proceeds" if "proceeds" in trades.columns else "value"
|
|
396
|
+
|
|
397
|
+
trades["underlying"] = trades.symbol.apply(
|
|
398
|
+
lambda x: x.split("_")[0]
|
|
399
|
+
if "_" in x
|
|
400
|
+
else "".join([i for i in x if not i.isdigit()])
|
|
401
|
+
)
|
|
402
|
+
ul_breakdown = trades.groupby("underlying").agg(
|
|
403
|
+
roundtrips=pd.NamedAgg(column="pnl", aggfunc="count"),
|
|
404
|
+
total_pnl=pd.NamedAgg(column="pnl", aggfunc="sum"),
|
|
405
|
+
total_turnover=pd.NamedAgg(
|
|
406
|
+
column=turnover_col_name, aggfunc=lambda x: x.abs().sum()
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
ul_breakdown["pnl_ratio"] = (
|
|
410
|
+
ul_breakdown["total_pnl"] / ul_breakdown["total_turnover"]
|
|
411
|
+
)
|
|
412
|
+
ul_breakdown = ul_breakdown.sort_values("total_pnl", ascending=False)
|
|
413
|
+
|
|
414
|
+
if slippage is not None:
|
|
415
|
+
slippage["underlying"] = slippage.symbol.apply(
|
|
416
|
+
lambda x: x.split("_")[0]
|
|
417
|
+
if "_" in x
|
|
418
|
+
else "".join([i for i in x if not i.isdigit()])
|
|
419
|
+
)
|
|
420
|
+
slippage_breakdown = slippage.groupby("underlying").agg(
|
|
421
|
+
total_slippage=pd.NamedAgg(column="slippage", aggfunc="sum"),
|
|
422
|
+
total_turnover=pd.NamedAgg(
|
|
423
|
+
column=turnover_col_name, aggfunc=lambda x: x.abs().sum()
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
slippage_breakdown["slippage_ratio"] = (
|
|
427
|
+
slippage_breakdown["total_slippage"]
|
|
428
|
+
/ slippage_breakdown["total_turnover"]
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Display dashboard
|
|
432
|
+
left_col, right_col = st.columns([5, 2])
|
|
433
|
+
|
|
434
|
+
with right_col:
|
|
435
|
+
st.markdown("##### Performance Metrics")
|
|
436
|
+
show_performance_metrics(returns, bm_returns, drawdowns, bm_drawdowns)
|
|
437
|
+
|
|
438
|
+
if rts is not None and len(rts) > 0:
|
|
439
|
+
st.write("")
|
|
440
|
+
st.markdown("##### Trade Metrics")
|
|
441
|
+
winners, losers = show_trade_metrics(
|
|
442
|
+
rts, trades, returns, slippage, turnover_col_name
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
with left_col:
|
|
446
|
+
if not cfg:
|
|
447
|
+
with st.expander("Account/Benchmark Full Names"):
|
|
448
|
+
records = []
|
|
449
|
+
for acct in selected_accounts:
|
|
450
|
+
records.append(["Account", acct])
|
|
451
|
+
records.append(["Benchmark", benchmark])
|
|
452
|
+
full_names = (
|
|
453
|
+
pd.DataFrame.from_records(records, columns=["Type", "Name"])
|
|
454
|
+
.set_index("Type")
|
|
455
|
+
.sort_index()
|
|
456
|
+
)
|
|
457
|
+
st.markdown(tabulate(full_names, headers="keys", tablefmt="github"))
|
|
458
|
+
|
|
459
|
+
plot_cumulative_returns(returns, bm_returns, bm_ratio)
|
|
460
|
+
plot_underwater(returns, bm_returns)
|
|
461
|
+
plot_yearly_returns(yearly_return, bm_yearly_return)
|
|
462
|
+
plot_monthly_returns_heatmap(returns)
|
|
463
|
+
plot_monthly_returns(monthly_return, bm_monthly_return)
|
|
464
|
+
plot_weekly_returns(weekly_return, bm_weekly_return)
|
|
465
|
+
|
|
466
|
+
if trades is not None and len(trades) > 0:
|
|
467
|
+
plot_profit_distribution(ul_breakdown)
|
|
468
|
+
|
|
469
|
+
if slippage is not None and len(slippage) > 0:
|
|
470
|
+
plot_slippage_distribution(slippage_breakdown)
|
|
471
|
+
|
|
472
|
+
with left_col:
|
|
473
|
+
st.markdown("### Top Drawdowns Table")
|
|
474
|
+
st.dataframe(drawdowns)
|
|
475
|
+
|
|
476
|
+
st.markdown("### Returns Tables")
|
|
477
|
+
daily_tab, weekly_tab, monthly_tab, yearly_tab = st.tabs(
|
|
478
|
+
["Daily", "Weekly", "Monthly", "Yearly"]
|
|
479
|
+
)
|
|
480
|
+
with daily_tab:
|
|
481
|
+
st.markdown("##### Daily Returns")
|
|
482
|
+
st.dataframe(
|
|
483
|
+
returns.drop(["benchmark_cum_returns", "benchmark_underwater"], axis=1)
|
|
484
|
+
.sort_index(ascending=False)
|
|
485
|
+
.style.format(precision=2)
|
|
486
|
+
.format("{:.2%}", subset=["returns", "cum_returns", "underwater"])
|
|
487
|
+
)
|
|
488
|
+
show_col_desc(returns, ["returns"])
|
|
489
|
+
|
|
490
|
+
with weekly_tab:
|
|
491
|
+
st.markdown("##### Weekly Returns")
|
|
492
|
+
sub_left_col, sub_right_col = weekly_tab.columns([4, 4])
|
|
493
|
+
with sub_left_col:
|
|
494
|
+
st.dataframe(weekly_return)
|
|
495
|
+
with sub_right_col:
|
|
496
|
+
show_col_desc(weekly_return, ["Return"])
|
|
497
|
+
plot_return_distribution(weekly_return, "weekly")
|
|
498
|
+
|
|
499
|
+
with monthly_tab:
|
|
500
|
+
st.markdown("##### Monthly Returns")
|
|
501
|
+
sub_left_col, sub_right_col = monthly_tab.columns([4, 4])
|
|
502
|
+
with sub_left_col:
|
|
503
|
+
st.dataframe(monthly_return)
|
|
504
|
+
with sub_right_col:
|
|
505
|
+
show_col_desc(monthly_return, ["Return"])
|
|
506
|
+
plot_return_distribution(monthly_return, "monthly")
|
|
507
|
+
|
|
508
|
+
with yearly_tab:
|
|
509
|
+
st.markdown("### Yearly Returns")
|
|
510
|
+
st.dataframe(yearly_return)
|
|
511
|
+
|
|
512
|
+
if rts is not None:
|
|
513
|
+
st.markdown("### Roundtrips Tables")
|
|
514
|
+
winners_tab, losers_tab, rts_tab = st.tabs(["Winners", "Losers", "All"])
|
|
515
|
+
with winners_tab:
|
|
516
|
+
if winners is not None:
|
|
517
|
+
st.markdown("##### Winners")
|
|
518
|
+
winners["duration"] = winners["duration"].dt.days
|
|
519
|
+
st.dataframe(
|
|
520
|
+
winners.style.format(precision=2).format(
|
|
521
|
+
"{:.2%}", subset=["pnl_pct", "account_pnl_pct"]
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
show_col_desc(winners, ["pnl_pct", "account_pnl_pct"])
|
|
525
|
+
show_col_desc(winners, ["duration"], dtype="int")
|
|
526
|
+
|
|
527
|
+
with losers_tab:
|
|
528
|
+
if losers is not None:
|
|
529
|
+
st.markdown("##### Losers")
|
|
530
|
+
losers["duration"] = losers["duration"].dt.days
|
|
531
|
+
st.dataframe(
|
|
532
|
+
losers.style.format(precision=2).format(
|
|
533
|
+
"{:.2%}", subset=["pnl_pct", "account_pnl_pct"]
|
|
534
|
+
)
|
|
535
|
+
)
|
|
536
|
+
show_col_desc(losers, ["pnl_pct", "account_pnl_pct"])
|
|
537
|
+
show_col_desc(losers, ["duration"], dtype="int")
|
|
538
|
+
|
|
539
|
+
with rts_tab:
|
|
540
|
+
st.markdown("##### All")
|
|
541
|
+
rts["duration"] = rts["duration"].dt.days
|
|
542
|
+
st.dataframe(
|
|
543
|
+
rts.style.format(precision=2).format(
|
|
544
|
+
"{:.2%}", subset=["pnl_pct", "account_pnl_pct"]
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
if trades is not None:
|
|
549
|
+
st.markdown("### Underlying Breakdown")
|
|
550
|
+
if slippage is not None:
|
|
551
|
+
ul_breakdown = ul_breakdown.merge(
|
|
552
|
+
slippage_breakdown.filter(["total_slippage", "slippage_ratio"]),
|
|
553
|
+
left_index=True,
|
|
554
|
+
right_index=True,
|
|
555
|
+
)
|
|
556
|
+
st.dataframe(
|
|
557
|
+
ul_breakdown.style.format(precision=0)
|
|
558
|
+
.format("{:.6f}", subset=["pnl_ratio", "slippage_ratio"])
|
|
559
|
+
.format(
|
|
560
|
+
"{:,.0f}",
|
|
561
|
+
subset=["total_pnl", "total_turnover", "total_slippage"],
|
|
562
|
+
)
|
|
563
|
+
)
|
|
564
|
+
else:
|
|
565
|
+
st.dataframe(
|
|
566
|
+
ul_breakdown.style.format(precision=0)
|
|
567
|
+
.format("{:.6f}", subset=["pnl_ratio"])
|
|
568
|
+
.format("{:,.0f}", subset=["total_pnl", "total_turnover"])
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
if investors is not None:
|
|
572
|
+
st.markdown("### Investors Tables")
|
|
573
|
+
names = list(investors["name"].unique())
|
|
574
|
+
# Filter out investors with no data in the selected period
|
|
575
|
+
available_investors = [
|
|
576
|
+
name
|
|
577
|
+
for name in names
|
|
578
|
+
if name in investor_returns and investor_returns[name] is not None
|
|
579
|
+
]
|
|
580
|
+
|
|
581
|
+
if not available_investors:
|
|
582
|
+
st.info("No investor data available for the selected date range.")
|
|
583
|
+
|
|
584
|
+
for idx, tab in enumerate(st.tabs(available_investors)):
|
|
585
|
+
investor = available_investors[idx]
|
|
586
|
+
ireturns = investor_returns[investor]
|
|
587
|
+
invested = ireturns.cashflow.sum()
|
|
588
|
+
|
|
589
|
+
icashflow = ireturns.filter(["date", "cashflow"]).reset_index()
|
|
590
|
+
icashflow = icashflow.where(icashflow.cashflow != 0).dropna()
|
|
591
|
+
icashflow.columns = ["Date", "Cashflow"]
|
|
592
|
+
icashflow = icashflow.set_index("Date")
|
|
593
|
+
|
|
594
|
+
start_value = ireturns.adj_last_eod_value.iloc[-1]
|
|
595
|
+
if np.isnan(start_value):
|
|
596
|
+
st.markdown("No data for the selected period.")
|
|
597
|
+
continue
|
|
598
|
+
|
|
599
|
+
current_value = ireturns.account_value.iloc[0]
|
|
600
|
+
|
|
601
|
+
with tab:
|
|
602
|
+
isummary_data = [
|
|
603
|
+
("Start Value", start_value),
|
|
604
|
+
("Net Invested", invested),
|
|
605
|
+
("Current Value", current_value),
|
|
606
|
+
("PnL", ireturns.today_pnl.sum()),
|
|
607
|
+
("Return", ireturns.cum_returns.iloc[0]),
|
|
608
|
+
("Annual Return", ep.annual_return(ireturns["returns"])),
|
|
609
|
+
("Max Drawdown", ireturns.underwater.min()),
|
|
610
|
+
]
|
|
611
|
+
isummary = pd.DataFrame.from_records(isummary_data).pivot_table(
|
|
612
|
+
values=1, columns=0, sort=False
|
|
613
|
+
)
|
|
614
|
+
isummary.index = [investor]
|
|
615
|
+
st.markdown("##### Summary")
|
|
616
|
+
st.dataframe(
|
|
617
|
+
isummary.style.format(precision=0)
|
|
618
|
+
.format(
|
|
619
|
+
"{:.2%}", subset=["Return", "Annual Return", "Max Drawdown"]
|
|
620
|
+
)
|
|
621
|
+
.format(
|
|
622
|
+
"{:,.0f}",
|
|
623
|
+
subset=[
|
|
624
|
+
"Start Value",
|
|
625
|
+
"Net Invested",
|
|
626
|
+
"Current Value",
|
|
627
|
+
"PnL",
|
|
628
|
+
],
|
|
629
|
+
)
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
st.write("")
|
|
633
|
+
st.markdown("##### Cashflow")
|
|
634
|
+
st.dataframe(icashflow.style.format("{:,.0f}"))
|
|
635
|
+
|
|
636
|
+
st.write("")
|
|
637
|
+
st.markdown("##### Daily Returns")
|
|
638
|
+
st.dataframe(
|
|
639
|
+
ireturns.style.format(precision=2).format(
|
|
640
|
+
"{:.2%}", subset=["returns", "cum_returns", "underwater"]
|
|
641
|
+
)
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
if __name__ == "__main__":
|
|
646
|
+
app()
|
stellar_stats/auth.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import extra_streamlit_components as stx
|
|
2
|
+
import streamlit as st
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def setup_authentication(cfg):
|
|
6
|
+
"""Setup authentication for the app using session state and cookies."""
|
|
7
|
+
# Initialize session state
|
|
8
|
+
if "authenticated" not in st.session_state:
|
|
9
|
+
st.session_state.authenticated = False
|
|
10
|
+
|
|
11
|
+
# Check cookie
|
|
12
|
+
cookie_manager = stx.CookieManager()
|
|
13
|
+
auth_cookie = cookie_manager.get("auth_cookie")
|
|
14
|
+
if auth_cookie == "authenticated":
|
|
15
|
+
st.session_state.authenticated = True
|
|
16
|
+
|
|
17
|
+
# Need authentication and not authenticated yet
|
|
18
|
+
if cfg and cfg["general"]["auth"] and not st.session_state.authenticated:
|
|
19
|
+
st.markdown("### Login")
|
|
20
|
+
passcode = st.text_input("Enter Passcode", type="password")
|
|
21
|
+
|
|
22
|
+
if passcode:
|
|
23
|
+
if passcode == cfg["general"]["passcode"]:
|
|
24
|
+
st.session_state.authenticated = True
|
|
25
|
+
cookie_manager.set(
|
|
26
|
+
"auth_cookie", "authenticated", max_age=30 * 24 * 60 * 60
|
|
27
|
+
)
|
|
28
|
+
st.rerun()
|
|
29
|
+
else:
|
|
30
|
+
st.error("Invalid passcode")
|
|
31
|
+
st.session_state.authenticated = False
|
|
32
|
+
|
|
33
|
+
# Stop execution if not authenticated
|
|
34
|
+
if not st.session_state.authenticated:
|
|
35
|
+
st.stop()
|