huggingface112 commited on
Commit
c121d97
1 Parent(s): 29f1ee3

fix return in total portfolio card

Browse files
Files changed (4) hide show
  1. appComponents.py +29 -64
  2. index_page.py +3 -3
  3. processing.py +56 -14
  4. 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.result.loc[self.result.time.idxmax()]
37
- active_return = most_recent_row.active_return
 
38
  tracking_error = most_recent_row.tracking_error
39
- total_return = most_recent_row.weighted_return_p
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.result, y=[
205
- 'weighted_return_p', 'weighted_return_b'])
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
- 'weighted_return_p': 'Portfolio回报',
211
- 'weighted_return_b': 'benchmark回报'
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
- self.result = self._create_result_df(self.p_stock_df, self.b_stock_df)
 
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.result = self._create_result_df(b_stock_df, p_stock_df)
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
- 'weighted_return': 'sum',
726
- 'cash': 'sum',
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
- # return df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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