Spaces:
Runtime error
Runtime error
huggingface112
commited on
Commit
•
c121d97
1
Parent(s):
29f1ee3
fix return in total portfolio card
Browse files- appComponents.py +29 -64
- index_page.py +3 -3
- processing.py +56 -14
- utils.py +10 -3
appComponents.py
CHANGED
@@ -11,11 +11,16 @@ import param
|
|
11 |
import styling
|
12 |
import description
|
13 |
import plotly.graph_objs as go
|
|
|
14 |
# import warnings
|
15 |
pn.extension('mathjax')
|
16 |
pn.extension('plotly')
|
17 |
|
|
|
18 |
class TotalReturnCard(Viewer):
|
|
|
|
|
|
|
19 |
|
20 |
start_date = param.Parameter()
|
21 |
end_date = param.Parameter()
|
@@ -33,10 +38,11 @@ class TotalReturnCard(Viewer):
|
|
33 |
# Calculate the risk, tracking error, active return
|
34 |
|
35 |
# get the result from entry with max time
|
36 |
-
most_recent_row = self.
|
37 |
-
|
|
|
38 |
tracking_error = most_recent_row.tracking_error
|
39 |
-
total_return = most_recent_row.
|
40 |
mkt_cap = most_recent_row.cash
|
41 |
risk = most_recent_row.risk
|
42 |
|
@@ -151,64 +157,16 @@ class TotalReturnCard(Viewer):
|
|
151 |
|
152 |
return report
|
153 |
|
154 |
-
def _create_result_df(self, analytic_b, analytic_p):
|
155 |
-
'''
|
156 |
-
calculate weighted return, tracking error, risk for the whole portfolio
|
157 |
-
'''
|
158 |
-
return_b_df = processing.calculate_weighted_return(
|
159 |
-
analytic_b, self.start_date, self.end_date)
|
160 |
-
return_p_df = processing.calculate_weighted_return(
|
161 |
-
analytic_p, self.start_date, self.end_date)
|
162 |
-
|
163 |
-
# weighted pct
|
164 |
-
processing.calculate_weighted_pct(return_b_df)
|
165 |
-
processing.calculate_weighted_pct(return_p_df)
|
166 |
-
|
167 |
-
# not needed but to accomendate post processing
|
168 |
-
return_b_df['in_benchmark'] = True
|
169 |
-
return_p_df['in_portfolio'] = False
|
170 |
-
merged_df = pd.merge(return_b_df, return_p_df, on=[
|
171 |
-
'ticker', 'time'], how='outer', suffixes=('_b', '_p'))
|
172 |
-
processing.post_process_merged_analytic_df(merged_df)
|
173 |
-
|
174 |
-
# fill emtpy weighted_return with 0
|
175 |
-
# merged_df['weighted_return_b'] = merged_df['weighted_return_b'].fillna(0)
|
176 |
-
# merged_df['weighted_return_p'] = merged_df['weighted_return_p'].fillna(0)
|
177 |
-
|
178 |
-
# aggregate on date
|
179 |
-
result = merged_df.groupby('time').aggregate({'weighted_return_p': 'sum',
|
180 |
-
'weighted_return_b': 'sum',
|
181 |
-
"cash": 'sum',
|
182 |
-
'weighted_pct_p': 'sum',
|
183 |
-
'weighted_pct_b': 'sum',
|
184 |
-
})
|
185 |
-
# active return
|
186 |
-
result['active_return'] = result.weighted_return_p - \
|
187 |
-
result.weighted_return_b
|
188 |
-
|
189 |
-
result.sort_values('time', inplace=True)
|
190 |
-
# tracking error
|
191 |
-
result['tracking_error'] = result['active_return'].rolling(
|
192 |
-
len(result), min_periods=1).std() * np.sqrt(252)
|
193 |
-
|
194 |
-
# risk std of pct
|
195 |
-
result['risk'] = result['weighted_pct_b'].rolling(
|
196 |
-
len(result), min_periods=1).std() * np.sqrt(252)
|
197 |
-
|
198 |
-
# result.time = result.index
|
199 |
-
result.reset_index(inplace=True)
|
200 |
-
return result
|
201 |
-
|
202 |
def create_plot(self):
|
203 |
|
204 |
-
fig = px.line(self.
|
205 |
-
'
|
206 |
fig.update_traces(mode="lines+markers",
|
207 |
marker=dict(size=5), line=dict(width=2))
|
208 |
fig.update_layout(styling.plot_layout)
|
209 |
colname_to_name = {
|
210 |
-
'
|
211 |
-
'
|
212 |
}
|
213 |
fig.for_each_trace(lambda t: t.update(name=colname_to_name.get(t.name, t.name),
|
214 |
legendgroup=colname_to_name.get(
|
@@ -219,9 +177,16 @@ class TotalReturnCard(Viewer):
|
|
219 |
# fig.layout.autosize = True
|
220 |
return fig.to_dict()
|
221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
222 |
@param.depends('start_date', 'end_date', 'b_stock_df', 'p_stock_df', watch=True)
|
223 |
def update(self):
|
224 |
-
|
|
|
225 |
fig = self.create_plot()
|
226 |
report = self.create_report()
|
227 |
self.report.object = report
|
@@ -239,7 +204,7 @@ class TotalReturnCard(Viewer):
|
|
239 |
)
|
240 |
self.start_date = self._date_range.value_start
|
241 |
self.end_date = self._date_range.value_end
|
242 |
-
self.
|
243 |
self.plot_pane = pn.pane.Plotly(
|
244 |
self.create_plot(), sizing_mode='stretch_width')
|
245 |
|
@@ -290,7 +255,7 @@ class DrawDownCard(Viewer):
|
|
290 |
'cash': 'sum',
|
291 |
'pnl': 'sum',
|
292 |
})
|
293 |
-
|
294 |
# calcualte cum pnl
|
295 |
agg_df['cum_pnl'] = agg_df['pnl'].cumsum()
|
296 |
|
@@ -716,21 +681,21 @@ class TopHeader(Viewer):
|
|
716 |
|
717 |
def _process(self):
|
718 |
'''calculate accumulative pnl, total return and Max Drawdown on return'''
|
719 |
-
|
720 |
# return
|
721 |
result_df = processing.calculate_weighted_return(self.eval_df)
|
722 |
-
|
723 |
# merge by date
|
724 |
agg_df = result_df.groupby('time').aggregate({
|
725 |
-
|
726 |
-
|
727 |
'pnl': 'sum',
|
728 |
-
|
729 |
agg_df.reset_index(inplace=True)
|
730 |
|
731 |
# accumulative pnl
|
732 |
agg_df['cum_pnl'] = agg_df['pnl'].cumsum()
|
733 |
-
|
734 |
# calcualte drawdown
|
735 |
result = processing.calculate_draw_down_on(agg_df)
|
736 |
max_draw_down = result.drawn_down.min()
|
|
|
11 |
import styling
|
12 |
import description
|
13 |
import plotly.graph_objs as go
|
14 |
+
import utils
|
15 |
# import warnings
|
16 |
pn.extension('mathjax')
|
17 |
pn.extension('plotly')
|
18 |
|
19 |
+
|
20 |
class TotalReturnCard(Viewer):
|
21 |
+
'''
|
22 |
+
summary on the portfolio performance vs benchmark performance
|
23 |
+
'''
|
24 |
|
25 |
start_date = param.Parameter()
|
26 |
end_date = param.Parameter()
|
|
|
38 |
# Calculate the risk, tracking error, active return
|
39 |
|
40 |
# get the result from entry with max time
|
41 |
+
most_recent_row = self.portfolio_df.loc[self.portfolio_df.period.idxmax(
|
42 |
+
)]
|
43 |
+
active_return = most_recent_row.cum_return_p - most_recent_row.cum_return_b
|
44 |
tracking_error = most_recent_row.tracking_error
|
45 |
+
total_return = most_recent_row.cum_return_p
|
46 |
mkt_cap = most_recent_row.cash
|
47 |
risk = most_recent_row.risk
|
48 |
|
|
|
157 |
|
158 |
return report
|
159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
def create_plot(self):
|
161 |
|
162 |
+
fig = px.line(self.portfolio_df, x='period', y=[
|
163 |
+
'cum_return_p', 'cum_return_b'])
|
164 |
fig.update_traces(mode="lines+markers",
|
165 |
marker=dict(size=5), line=dict(width=2))
|
166 |
fig.update_layout(styling.plot_layout)
|
167 |
colname_to_name = {
|
168 |
+
'cum_return_p': 'Portfolio回报',
|
169 |
+
'cum_return_b': 'benchmark回报'
|
170 |
}
|
171 |
fig.for_each_trace(lambda t: t.update(name=colname_to_name.get(t.name, t.name),
|
172 |
legendgroup=colname_to_name.get(
|
|
|
177 |
# fig.layout.autosize = True
|
178 |
return fig.to_dict()
|
179 |
|
180 |
+
def create_portfolio_df(self):
|
181 |
+
clip_p = utils.clip_df(self.start_date, self.end_date, self.p_stock_df)
|
182 |
+
clip_b = utils.clip_df(self.start_date, self.end_date, self.b_stock_df)
|
183 |
+
return processing.get_portfolio_anlaysis(analytic_b=clip_b,
|
184 |
+
analytic_p=clip_p)
|
185 |
+
|
186 |
@param.depends('start_date', 'end_date', 'b_stock_df', 'p_stock_df', watch=True)
|
187 |
def update(self):
|
188 |
+
|
189 |
+
self.portfolio_df = self.create_portfolio_df()
|
190 |
fig = self.create_plot()
|
191 |
report = self.create_report()
|
192 |
self.report.object = report
|
|
|
204 |
)
|
205 |
self.start_date = self._date_range.value_start
|
206 |
self.end_date = self._date_range.value_end
|
207 |
+
self.portfolio_df = self.create_portfolio_df()
|
208 |
self.plot_pane = pn.pane.Plotly(
|
209 |
self.create_plot(), sizing_mode='stretch_width')
|
210 |
|
|
|
255 |
'cash': 'sum',
|
256 |
'pnl': 'sum',
|
257 |
})
|
258 |
+
|
259 |
# calcualte cum pnl
|
260 |
agg_df['cum_pnl'] = agg_df['pnl'].cumsum()
|
261 |
|
|
|
681 |
|
682 |
def _process(self):
|
683 |
'''calculate accumulative pnl, total return and Max Drawdown on return'''
|
684 |
+
|
685 |
# return
|
686 |
result_df = processing.calculate_weighted_return(self.eval_df)
|
687 |
+
|
688 |
# merge by date
|
689 |
agg_df = result_df.groupby('time').aggregate({
|
690 |
+
'weighted_return': 'sum',
|
691 |
+
'cash': 'sum',
|
692 |
'pnl': 'sum',
|
693 |
+
})
|
694 |
agg_df.reset_index(inplace=True)
|
695 |
|
696 |
# accumulative pnl
|
697 |
agg_df['cum_pnl'] = agg_df['pnl'].cumsum()
|
698 |
+
|
699 |
# calcualte drawdown
|
700 |
result = processing.calculate_draw_down_on(agg_df)
|
701 |
max_draw_down = result.drawn_down.min()
|
index_page.py
CHANGED
@@ -7,10 +7,8 @@ import db_operation as db
|
|
7 |
pn.extension('mathjax')
|
8 |
|
9 |
pn.extension('plotly')
|
10 |
-
pn.extension('tabulator')
|
11 |
-
db_url = 'sqlite:///instance/local.db'
|
12 |
-
engine = create_engine(db_url)
|
13 |
|
|
|
14 |
|
15 |
analytic_p = db.get_portfolio_analytic_df()
|
16 |
analytic_b = db.get_benchmark_analytic_df()
|
@@ -47,6 +45,8 @@ else:
|
|
47 |
|
48 |
template = pn.template.FastListTemplate(
|
49 |
title="Portfolio一览",
|
|
|
|
|
50 |
# sidebar=[freq, phase],
|
51 |
)
|
52 |
template.sidebar.append(SideNavBar())
|
|
|
7 |
pn.extension('mathjax')
|
8 |
|
9 |
pn.extension('plotly')
|
|
|
|
|
|
|
10 |
|
11 |
+
pn.extension('tabulator')
|
12 |
|
13 |
analytic_p = db.get_portfolio_analytic_df()
|
14 |
analytic_b = db.get_benchmark_analytic_df()
|
|
|
45 |
|
46 |
template = pn.template.FastListTemplate(
|
47 |
title="Portfolio一览",
|
48 |
+
side_bar_width=200,
|
49 |
+
collapsed_sidebar = True,
|
50 |
# sidebar=[freq, phase],
|
51 |
)
|
52 |
template.sidebar.append(SideNavBar())
|
processing.py
CHANGED
@@ -335,9 +335,9 @@ def calculate_log_return(df: pd.DataFrame):
|
|
335 |
grouped = inter_df.groupby('ticker')
|
336 |
inter_df['prev_w'] = grouped['weight'].shift(1)
|
337 |
inter_df['prev_close'] = grouped['close'].shift(1)
|
|
|
|
|
338 |
inter_df['log_return'] = np.log(inter_df['close'] / inter_df['prev_close'])
|
339 |
-
inter_df['weighted_log_return'] = inter_df['log_return'] * \
|
340 |
-
inter_df['prev_w']
|
341 |
# patch
|
342 |
df['log_return'] = inter_df['log_return']
|
343 |
df['weighted_log_return'] = inter_df['weighted_log_return']
|
@@ -426,10 +426,10 @@ def create_analytic_df(price_df, profile_df):
|
|
426 |
# daily stock price use begin of the date, need to convert profile_df day to begin of the date
|
427 |
profile_df['time'] = profile_df['time'].map(
|
428 |
lambda x: datetime(x.year, x.month, x.day))
|
429 |
-
|
430 |
# make every time entry the same dimension
|
431 |
uni_profile_df = _uniformize_time_series(profile_df)
|
432 |
-
|
433 |
# TODO handle rename column here
|
434 |
df = price_df.merge(uni_profile_df, on=['ticker', 'time'], how='outer')
|
435 |
df.sort_values(by=['ticker', 'time'], inplace=True)
|
@@ -464,8 +464,6 @@ def calculate_attributes_between_dates(start, end, calculated_p_stock, calculate
|
|
464 |
b_ranged_df = calculated_b_stock[(calculated_b_stock.date >= start) & (
|
465 |
calculated_b_stock.date <= end)]
|
466 |
|
467 |
-
# return and weight of portfolio
|
468 |
-
p_start_df = p_ranged_df[p_ranged_df.date == p_ranged_df.date.min()]
|
469 |
p_end_df = p_ranged_df[p_ranged_df.date == p_ranged_df.date.max()]
|
470 |
p_concat = pd.concat([p_start_df, p_end_df])
|
471 |
# pct is unweighted return
|
@@ -812,12 +810,56 @@ def calculate_draw_down_on(df, key='weighted_return'):
|
|
812 |
|
813 |
return df
|
814 |
|
815 |
-
# def calculate_accumulative_pnl(df):
|
816 |
-
# '''
|
817 |
-
# calculate accumulative pnl on analytic df
|
818 |
-
# '''
|
819 |
-
# df = df.sort_values(by=['time'])
|
820 |
-
# df['accumulative_pnl'] = df.groupby('ticker')['pnl'].rolling(
|
821 |
|
822 |
-
|
823 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
335 |
grouped = inter_df.groupby('ticker')
|
336 |
inter_df['prev_w'] = grouped['weight'].shift(1)
|
337 |
inter_df['prev_close'] = grouped['close'].shift(1)
|
338 |
+
inter_df['weighted_log_return'] = np.log(
|
339 |
+
(inter_df['close'] / inter_df['prev_close']) * inter_df['prev_w'])
|
340 |
inter_df['log_return'] = np.log(inter_df['close'] / inter_df['prev_close'])
|
|
|
|
|
341 |
# patch
|
342 |
df['log_return'] = inter_df['log_return']
|
343 |
df['weighted_log_return'] = inter_df['weighted_log_return']
|
|
|
426 |
# daily stock price use begin of the date, need to convert profile_df day to begin of the date
|
427 |
profile_df['time'] = profile_df['time'].map(
|
428 |
lambda x: datetime(x.year, x.month, x.day))
|
429 |
+
|
430 |
# make every time entry the same dimension
|
431 |
uni_profile_df = _uniformize_time_series(profile_df)
|
432 |
+
|
433 |
# TODO handle rename column here
|
434 |
df = price_df.merge(uni_profile_df, on=['ticker', 'time'], how='outer')
|
435 |
df.sort_values(by=['ticker', 'time'], inplace=True)
|
|
|
464 |
b_ranged_df = calculated_b_stock[(calculated_b_stock.date >= start) & (
|
465 |
calculated_b_stock.date <= end)]
|
466 |
|
|
|
|
|
467 |
p_end_df = p_ranged_df[p_ranged_df.date == p_ranged_df.date.max()]
|
468 |
p_concat = pd.concat([p_start_df, p_end_df])
|
469 |
# pct is unweighted return
|
|
|
810 |
|
811 |
return df
|
812 |
|
|
|
|
|
|
|
|
|
|
|
|
|
813 |
|
814 |
+
def _daily_return(df: pd.DataFrame):
|
815 |
+
'''
|
816 |
+
patch df with daily return
|
817 |
+
helper function for get_portfolio_anlaysis
|
818 |
+
'''
|
819 |
+
prev_ws = df.groupby('ticker')['weight'].shift(1)
|
820 |
+
df['return'] = df.pct * prev_ws
|
821 |
+
|
822 |
+
|
823 |
+
def _agg_on_day(df: pd.DataFrame):
|
824 |
+
df['period'] = df.time.dt.to_period('D')
|
825 |
+
on_column = {'return': 'sum'}
|
826 |
+
if 'cash' in df.columns:
|
827 |
+
on_column['cash'] = 'sum'
|
828 |
+
if 'pnl' in df.columns:
|
829 |
+
on_column['pnl'] = 'sum'
|
830 |
+
|
831 |
+
agg_df = df.groupby('period').agg(on_column)
|
832 |
+
return agg_df.reset_index()
|
833 |
+
|
834 |
+
|
835 |
+
def get_portfolio_anlaysis(analytic_p, analytic_b):
|
836 |
+
'''
|
837 |
+
return df contain daily pnl, daily return, accumulative return
|
838 |
+
risk and tracking error of portfolio and benchmark
|
839 |
+
'''
|
840 |
+
|
841 |
+
# daily return(weighted pct)
|
842 |
+
_daily_return(analytic_p)
|
843 |
+
_daily_return(analytic_b)
|
844 |
+
|
845 |
+
# aggregate to daily
|
846 |
+
agg_p = _agg_on_day(analytic_p)
|
847 |
+
agg_b = _agg_on_day(analytic_b)
|
848 |
+
|
849 |
+
# accumulative return
|
850 |
+
agg_p['cum_return'] = (agg_p['return']+1).cumprod() - 1
|
851 |
+
agg_b['cum_return'] = (agg_b['return']+1).cumprod() - 1
|
852 |
+
|
853 |
+
# merge
|
854 |
+
merged_df = pd.merge(
|
855 |
+
agg_p, agg_b, on=['period'], how='outer', suffixes=('_p', '_b'))
|
856 |
+
merged_df.sort_values('period', inplace=True)
|
857 |
+
|
858 |
+
# risk, using population deviation
|
859 |
+
merged_df['risk'] = merged_df['return_p'].expanding(min_periods=1).std()
|
860 |
+
|
861 |
+
# tracking error
|
862 |
+
merged_df['tracking_error'] = (
|
863 |
+
merged_df['return_p'] - merged_df['return_b']).expanding(min_periods=1).std()
|
864 |
+
|
865 |
+
return merged_df
|
utils.py
CHANGED
@@ -8,6 +8,13 @@ from sqlalchemy import create_engine
|
|
8 |
db_url = 'sqlite:///instance/local.db'
|
9 |
|
10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
def time_in_beijing(strip_time_zone=True):
|
12 |
'''
|
13 |
return current time in Beijing as datetime object
|
@@ -70,14 +77,14 @@ def create_stocks_entry_from_excel(byte_string):
|
|
70 |
'''
|
71 |
uploaded_df = None
|
72 |
with io.BytesIO(byte_string) as f:
|
73 |
-
uploaded_df = pd.read_excel(f)
|
74 |
|
75 |
# throw exception if doesn't have required columns
|
76 |
if not set(['证券代码', '持仓数量', '平均建仓成本', 'time_stamp']).issubset(uploaded_df.columns):
|
77 |
raise Exception('Missing required columns')
|
78 |
# print(uploaded_df)
|
79 |
# uploaded_df = pd.read_excel()
|
80 |
-
uploaded_df.drop(columns='Unnamed: 0', inplace=True)
|
81 |
# Define the regular expression pattern to match the string endings
|
82 |
pattern = r'\.(sz|sh)$'
|
83 |
# Define the replacement strings for each match group
|
@@ -153,7 +160,7 @@ def create_html_report(result: list[tuple]):
|
|
153 |
title: str, title to display
|
154 |
value: any, value to display
|
155 |
type: str, used to format value
|
156 |
-
|
157 |
Returns
|
158 |
-------
|
159 |
html: str
|
|
|
8 |
db_url = 'sqlite:///instance/local.db'
|
9 |
|
10 |
|
11 |
+
def clip_df(start, end, df: pd.DataFrame, on='time'):
|
12 |
+
'''
|
13 |
+
return a copy of df between start and end date inclusive
|
14 |
+
'''
|
15 |
+
return df[df.time.between(start, end, inclusive='both')].copy()
|
16 |
+
|
17 |
+
|
18 |
def time_in_beijing(strip_time_zone=True):
|
19 |
'''
|
20 |
return current time in Beijing as datetime object
|
|
|
77 |
'''
|
78 |
uploaded_df = None
|
79 |
with io.BytesIO(byte_string) as f:
|
80 |
+
uploaded_df = pd.read_excel(f, index_col=None)
|
81 |
|
82 |
# throw exception if doesn't have required columns
|
83 |
if not set(['证券代码', '持仓数量', '平均建仓成本', 'time_stamp']).issubset(uploaded_df.columns):
|
84 |
raise Exception('Missing required columns')
|
85 |
# print(uploaded_df)
|
86 |
# uploaded_df = pd.read_excel()
|
87 |
+
# uploaded_df.drop(columns='Unnamed: 0', inplace=True)
|
88 |
# Define the regular expression pattern to match the string endings
|
89 |
pattern = r'\.(sz|sh)$'
|
90 |
# Define the replacement strings for each match group
|
|
|
160 |
title: str, title to display
|
161 |
value: any, value to display
|
162 |
type: str, used to format value
|
163 |
+
|
164 |
Returns
|
165 |
-------
|
166 |
html: str
|