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.
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()