huggingface112 commited on
Commit
b9d37d3
1 Parent(s): a77544c

migrate drawDownCard to new pipeline

Browse files
Files changed (8) hide show
  1. .editorconfig +59 -0
  2. appComponents.py +151 -76
  3. index_page.py +7 -7
  4. instance/local.db +2 -2
  5. instance/log.json +1 -1
  6. pipeline.py +1 -1
  7. processing.py +79 -13
  8. utils.py +72 -0
.editorconfig ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [*]
2
+ cpp_indent_braces=false
3
+ cpp_indent_multi_line_relative_to=innermost_parenthesis
4
+ cpp_indent_within_parentheses=indent
5
+ cpp_indent_preserve_within_parentheses=false
6
+ cpp_indent_case_labels=false
7
+ cpp_indent_case_contents=true
8
+ cpp_indent_case_contents_when_block=false
9
+ cpp_indent_lambda_braces_when_parameter=true
10
+ cpp_indent_goto_labels=one_left
11
+ cpp_indent_preprocessor=leftmost_column
12
+ cpp_indent_access_specifiers=false
13
+ cpp_indent_namespace_contents=true
14
+ cpp_indent_preserve_comments=false
15
+ cpp_new_line_before_open_brace_namespace=ignore
16
+ cpp_new_line_before_open_brace_type=ignore
17
+ cpp_new_line_before_open_brace_function=ignore
18
+ cpp_new_line_before_open_brace_block=ignore
19
+ cpp_new_line_before_open_brace_lambda=ignore
20
+ cpp_new_line_scope_braces_on_separate_lines=false
21
+ cpp_new_line_close_brace_same_line_empty_type=false
22
+ cpp_new_line_close_brace_same_line_empty_function=false
23
+ cpp_new_line_before_catch=true
24
+ cpp_new_line_before_else=true
25
+ cpp_new_line_before_while_in_do_while=false
26
+ cpp_space_before_function_open_parenthesis=remove
27
+ cpp_space_within_parameter_list_parentheses=false
28
+ cpp_space_between_empty_parameter_list_parentheses=false
29
+ cpp_space_after_keywords_in_control_flow_statements=true
30
+ cpp_space_within_control_flow_statement_parentheses=false
31
+ cpp_space_before_lambda_open_parenthesis=false
32
+ cpp_space_within_cast_parentheses=false
33
+ cpp_space_after_cast_close_parenthesis=false
34
+ cpp_space_within_expression_parentheses=false
35
+ cpp_space_before_block_open_brace=true
36
+ cpp_space_between_empty_braces=false
37
+ cpp_space_before_initializer_list_open_brace=false
38
+ cpp_space_within_initializer_list_braces=true
39
+ cpp_space_preserve_in_initializer_list=true
40
+ cpp_space_before_open_square_bracket=false
41
+ cpp_space_within_square_brackets=false
42
+ cpp_space_before_empty_square_brackets=false
43
+ cpp_space_between_empty_square_brackets=false
44
+ cpp_space_group_square_brackets=true
45
+ cpp_space_within_lambda_brackets=false
46
+ cpp_space_between_empty_lambda_brackets=false
47
+ cpp_space_before_comma=false
48
+ cpp_space_after_comma=true
49
+ cpp_space_remove_around_member_operators=true
50
+ cpp_space_before_inheritance_colon=true
51
+ cpp_space_before_constructor_colon=true
52
+ cpp_space_remove_before_semicolon=true
53
+ cpp_space_after_semicolon=false
54
+ cpp_space_remove_around_unary_operator=true
55
+ cpp_space_around_binary_operator=insert
56
+ cpp_space_around_assignment_operator=insert
57
+ cpp_space_pointer_reference_alignment=left
58
+ cpp_space_around_ternary_operator=insert
59
+ cpp_wrap_preserve_blocks=one_liners
appComponents.py CHANGED
@@ -213,16 +213,11 @@ def create_hvplot_explore(calculated_b_stock, calculated_p_stock, p_eval_df, sec
213
 
214
  class TotalReturnCard(Viewer):
215
 
216
- value = param.Range(doc="A numeric range.")
217
- width = param.Integer(default=300)
218
  start_date = param.Parameter()
219
  end_date = param.Parameter()
220
- eval_df = param.Parameter()
221
  b_stock_df = param.Parameter()
222
  p_stock_df = param.Parameter()
223
- selected_df = param.Parameter()
224
- plot_pane = param.Parameter()
225
- report = param.Parameter()
226
 
227
  def format_number(self, num):
228
  return f'{round(num * 100, 2)}%'
@@ -231,31 +226,38 @@ class TotalReturnCard(Viewer):
231
  return 'green' if num >= 0 else 'red'
232
 
233
  def create_report(self):
234
- # Calculate the total return and risk
235
- result = processing.calculate_norm_return(
236
- self.eval_df, self.start_date, self.end_date)
237
- most_recent_row = result.tail(1)
238
- active_return = most_recent_row.active_return.values[0]
239
- tracking_error = result.active_return.std() * np.sqrt(252)
240
- total_return = most_recent_row.return_p.values[0]
241
- mkt_cap = most_recent_row.mkt_cap.values[0]
242
- risk = result['return_b'].std() * np.sqrt(252)
243
 
244
  # Calculate the total attribution
245
- attributes = processing.calculate_attributes_between_dates(
246
- self.start_date, self.end_date, self.p_stock_df, self.b_stock_df)
247
- total_attributes = attributes.aggregate({
248
- 'interaction': 'sum',
249
- 'allocation': 'sum',
250
- 'selection': 'sum',
251
- 'active_return': 'sum',
252
- 'notional_return': 'sum'
253
- })
254
- active_return_from_stock = total_attributes.active_return
255
- notional_return = total_attributes.notional_return
256
- interaction = total_attributes.interaction
257
- allocation = total_attributes.allocation
258
- selection = total_attributes.selection
 
 
 
 
 
 
 
259
 
260
  # Create a function for text report
261
  report = f"""
@@ -345,16 +347,64 @@ class TotalReturnCard(Viewer):
345
 
346
  return report
347
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  def create_plot(self):
349
- result = processing.calculate_norm_return(
350
- self.eval_df, self.start_date, self.end_date)
351
- fig = px.line(result, x="date", y=['return_p', 'return_b'])
352
  fig.update_traces(mode="lines+markers",
353
  marker=dict(size=5), line=dict(width=2))
354
  fig.update_layout(styling.plot_layout)
355
  colname_to_name = {
356
- 'return_p': 'Portfolio回报',
357
- 'return_b': 'benchmark回报'
358
  }
359
  fig.for_each_trace(lambda t: t.update(name=colname_to_name.get(t.name, t.name),
360
  legendgroup=colname_to_name.get(
@@ -365,31 +415,34 @@ class TotalReturnCard(Viewer):
365
  # fig.layout.autosize = True
366
  return fig.to_dict()
367
 
368
- @param.depends('start_date', 'end_date', 'eval_df', watch=True)
369
  def update(self):
 
370
  fig = self.create_plot()
371
  report = self.create_report()
372
  self.report.object = report
373
  self.plot_pane.object = fig
374
 
375
- def __init__(self, eval_df, b_stock_df, p_stock_df, **params):
376
- self.eval_df = eval_df
377
  self.b_stock_df = b_stock_df
378
  self.p_stock_df = p_stock_df
379
  self._date_range = pn.widgets.DateRangeSlider(
380
- start=eval_df.date.min(),
381
- end=eval_df.date.max(),
382
- value=(eval_df.date.max() - timedelta(days=7), eval_df.date.max())
 
383
  )
384
  self.start_date = self._date_range.value_start
385
  self.end_date = self._date_range.value_end
 
386
  self.plot_pane = pn.pane.Plotly(
387
  self.create_plot(), sizing_mode='stretch_width')
388
 
389
  self.report = pn.pane.HTML(
390
  self.create_report(), sizing_mode='stretch_width')
391
  super().__init__(**params)
392
- self._sync_widgets()
393
 
394
  def __panel__(self):
395
  self._layout = pn.Card(self._date_range, self.report, self.plot_pane,
@@ -397,15 +450,9 @@ class TotalReturnCard(Viewer):
397
  pn.widgets.TooltipIcon(value=description.summary_card)))
398
  return self._layout
399
 
400
- @param.depends('start_date', 'end_date', 'eval_df', watch=True)
401
- def update_selected_df(self):
402
- self.selected_df = self.eval_df[self.eval_df.date.between(
403
- self.start_date, self.end_date
404
- )]
405
-
406
- @param.depends('value', 'width', watch=True)
407
- def _sync_widgets(self):
408
- pass
409
 
410
  @param.depends('_date_range.value', watch=True)
411
  def _sync_params(self):
@@ -414,30 +461,53 @@ class TotalReturnCard(Viewer):
414
 
415
 
416
  class DrawDownCard(Viewer):
417
- def __init__(self, eval_df, calculated_p_stock, calculated_b_stock, **params):
418
- self.eval_df = eval_df
 
 
 
 
419
  self.calculated_p_stock = calculated_p_stock
420
- self.calculated_b_stock = calculated_b_stock
421
  self.drawdown_plot = pn.pane.Plotly(self.plot_drawdown())
422
  super().__init__(**params)
423
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  def calculate_drawdown(self):
425
- df = self.eval_df.copy()
426
- # rolling max return
427
- df['rolling_max_return_p'] = df['portfolio_return_p'].rolling(
428
- window=len(df), min_periods=1).max()
429
- # calculate drawdown
430
- df['drawn_down'] = abs(
431
- (1 + df.portfolio_return_p) / (1 + df.rolling_max_return_p) - 1)
432
  return df
433
 
434
  def plot_drawdown(self):
435
  df = self.calculate_drawdown()
436
- fig = px.line(df, x="date", y=['drawn_down'])
 
437
  # add scatter to represetn new high
438
- new_height_pnl = df[df.portfolio_return_p == df.rolling_max_return_p]
439
- fig.add_trace(go.Scatter(
440
- x=new_height_pnl['date'], y=new_height_pnl['drawn_down'], mode='markers', name='新的最高总回报'))
 
441
  colname_to_name = {
442
  'drawn_down': '回撤'
443
  }
@@ -450,14 +520,17 @@ class DrawDownCard(Viewer):
450
  ))
451
  return fig
452
 
 
453
  def update(self):
454
- pass
455
 
456
  def __panel__(self):
457
- self._layout = pn.Card(self.drawdown_plot,
458
- header=pn.Row(pn.pane.Str('回撤分析')),
459
- width=500
460
- )
 
 
461
  return self._layout
462
 
463
 
@@ -467,7 +540,7 @@ class HistReturnCard(Viewer):
467
  calculated_b_stock = param.Parameterized()
468
  calculated_p_stock = param.Parameterized()
469
  select_resolution = param.ObjectSelector(
470
- default='每月回报', objects=['每日回报', '每周回报', '每月回报', '每年回报'])
471
 
472
  def _calculate_return(self, df, freq):
473
  # start on tuesday, end on monday
@@ -516,7 +589,7 @@ class HistReturnCard(Viewer):
516
  hovertemplate=t.hovertemplate.replace(
517
  t.name, colname_to_name.get(t.name, t.name))
518
  ))
519
-
520
  fig.update_layout(barmode='group', title='主动回报归因',
521
  bargap=0.0, bargroupgap=0.0)
522
  fig.update_layout(**styling.plot_layout)
@@ -532,8 +605,8 @@ class HistReturnCard(Viewer):
532
  )
533
  # update legend
534
  colname_to_name = {
535
- 'portfolio_return_p': 'portfolio回报率',
536
- 'portfolio_return_b': 'benchmark回报率'
537
  }
538
  fig.for_each_trace(lambda t: t.update(name=colname_to_name.get(t.name, t.name),
539
  legendgroup=colname_to_name.get(
@@ -565,13 +638,15 @@ class HistReturnCard(Viewer):
565
  freq = 'Y'
566
  elif self.select_resolution == "每周回报":
567
  freq = 'W-MON'
568
- agg_p = processing.aggregate_analytic_df_by_period(self.calculated_p_stock, freq)
569
- agg_b = processing.aggregate_analytic_df_by_period(self.calculated_b_stock, freq)
 
 
570
  bhb_df = processing.calculate_periodic_BHB(agg_p, agg_b)
571
  agg_bhb = processing.aggregate_bhb_df(bhb_df)
572
  agg_bhb['period_str'] = agg_bhb.index.map(lambda x: str(x))
573
  return agg_bhb
574
-
575
  def __init__(self, calculated_p_stock, calculated_b_stock, **params):
576
  self.calculated_p_stock = calculated_p_stock
577
  self.calculated_b_stock = calculated_b_stock
 
213
 
214
  class TotalReturnCard(Viewer):
215
 
 
 
216
  start_date = param.Parameter()
217
  end_date = param.Parameter()
218
+
219
  b_stock_df = param.Parameter()
220
  p_stock_df = param.Parameter()
 
 
 
221
 
222
  def format_number(self, num):
223
  return f'{round(num * 100, 2)}%'
 
226
  return 'green' if num >= 0 else 'red'
227
 
228
  def create_report(self):
229
+ # Calculate the risk, tracking error, active return
230
+
231
+ # get the result from entry with max time
232
+ most_recent_row = self.result.loc[self.result.time.idxmax()]
233
+ active_return = most_recent_row.active_return
234
+ tracking_error = most_recent_row.tracking_error
235
+ total_return = most_recent_row.weighted_return_p
236
+ mkt_cap = most_recent_row.cash
237
+ risk = most_recent_row.risk
238
 
239
  # Calculate the total attribution
240
+ # attributes = processing.calculate_attributes_between_dates(
241
+ # self.start_date, self.end_date, self.p_stock_df, self.b_stock_df)
242
+ # total_attributes = attributes.aggregate({
243
+ # 'interaction': 'sum',
244
+ # 'allocation': 'sum',
245
+ # 'selection': 'sum',
246
+ # 'active_return': 'sum',
247
+ # 'notional_return': 'sum'
248
+ # })
249
+ # active_return_from_stock = total_attributes.active_return
250
+ # notional_return = total_attributes.notional_return
251
+ # interaction = total_attributes.interaction
252
+ # allocation = total_attributes.allocation
253
+ # selection = total_attributes.selection
254
+
255
+ # TODO dummy data
256
+ active_return_from_stock = 0
257
+ notional_return = 0
258
+ interaction = 0
259
+ allocation = 0
260
+ selection = 0
261
 
262
  # Create a function for text report
263
  report = f"""
 
347
 
348
  return report
349
 
350
+ def _create_result_df(self, analytic_b, analytic_p):
351
+ '''
352
+ calculate weighted return, tracking error, risk for the whole portfolio
353
+ '''
354
+ return_b_df = processing.calculate_weighted_return(
355
+ analytic_b, self.start_date, self.end_date)
356
+ return_p_df = processing.calculate_weighted_return(
357
+ analytic_p, self.start_date, self.end_date)
358
+
359
+ # weighted pct
360
+ processing.calculate_weighted_pct(return_b_df)
361
+ processing.calculate_weighted_pct(return_p_df)
362
+
363
+ # not needed but to accomendate post processing
364
+ return_b_df['in_benchmark'] = True
365
+ return_p_df['in_portfolio'] = False
366
+ merged_df = pd.merge(return_b_df, return_p_df, on=[
367
+ 'ticker', 'time'], how='outer', suffixes=('_b', '_p'))
368
+ processing.post_process_merged_analytic_df(merged_df)
369
+
370
+ # fill emtpy weighted_return with 0
371
+ # merged_df['weighted_return_b'] = merged_df['weighted_return_b'].fillna(0)
372
+ # merged_df['weighted_return_p'] = merged_df['weighted_return_p'].fillna(0)
373
+
374
+ # aggregate on date
375
+ result = merged_df.groupby('time').aggregate({'weighted_return_p': 'sum',
376
+ 'weighted_return_b': 'sum',
377
+ "cash": 'sum',
378
+ 'weighted_pct_p': 'sum',
379
+ 'weighted_pct_b': 'sum',
380
+ })
381
+ # active return
382
+ result['active_return'] = result.weighted_return_p - \
383
+ result.weighted_return_b
384
+
385
+ result.sort_values('time', inplace=True)
386
+ # tracking error
387
+ result['tracking_error'] = result['active_return'].rolling(
388
+ len(result), min_periods=1).std() * np.sqrt(252)
389
+
390
+ # risk std of pct
391
+ result['risk'] = result['weighted_pct_b'].rolling(
392
+ len(result), min_periods=1).std() * np.sqrt(252)
393
+
394
+ # result.time = result.index
395
+ result.reset_index(inplace=True)
396
+ return result
397
+
398
  def create_plot(self):
399
+
400
+ fig = px.line(self.result, y=[
401
+ 'weighted_return_p', 'weighted_return_b'])
402
  fig.update_traces(mode="lines+markers",
403
  marker=dict(size=5), line=dict(width=2))
404
  fig.update_layout(styling.plot_layout)
405
  colname_to_name = {
406
+ 'weighted_return_p': 'Portfolio回报',
407
+ 'weighted_return_b': 'benchmark回报'
408
  }
409
  fig.for_each_trace(lambda t: t.update(name=colname_to_name.get(t.name, t.name),
410
  legendgroup=colname_to_name.get(
 
415
  # fig.layout.autosize = True
416
  return fig.to_dict()
417
 
418
+ @param.depends('start_date', 'end_date', 'b_stock_df', 'p_stock_df', watch=True)
419
  def update(self):
420
+ self.result = self._create_result_df(self.p_stock_df, self.b_stock_df)
421
  fig = self.create_plot()
422
  report = self.create_report()
423
  self.report.object = report
424
  self.plot_pane.object = fig
425
 
426
+ def __init__(self, b_stock_df, p_stock_df, **params):
427
+
428
  self.b_stock_df = b_stock_df
429
  self.p_stock_df = p_stock_df
430
  self._date_range = pn.widgets.DateRangeSlider(
431
+ start=p_stock_df.time.min(),
432
+ end=b_stock_df.time.max(),
433
+ value=(p_stock_df.time.max() -
434
+ timedelta(days=7), p_stock_df.time.max())
435
  )
436
  self.start_date = self._date_range.value_start
437
  self.end_date = self._date_range.value_end
438
+ self.result = self._create_result_df(b_stock_df, p_stock_df)
439
  self.plot_pane = pn.pane.Plotly(
440
  self.create_plot(), sizing_mode='stretch_width')
441
 
442
  self.report = pn.pane.HTML(
443
  self.create_report(), sizing_mode='stretch_width')
444
  super().__init__(**params)
445
+ # self._sync_widgets()
446
 
447
  def __panel__(self):
448
  self._layout = pn.Card(self._date_range, self.report, self.plot_pane,
 
450
  pn.widgets.TooltipIcon(value=description.summary_card)))
451
  return self._layout
452
 
453
+ # @param.depends('value', 'width', watch=True)
454
+ # def _sync_widgets(self):
455
+ # pass
 
 
 
 
 
 
456
 
457
  @param.depends('_date_range.value', watch=True)
458
  def _sync_params(self):
 
461
 
462
 
463
  class DrawDownCard(Viewer):
464
+ selected_key_column = param.Parameter()
465
+ calcualted_p_stock = param.Parameter()
466
+
467
+ def __init__(self, calculated_p_stock, **params):
468
+ self.select = pn.widgets.Select(
469
+ name='Select', value='盈利', options=['盈利', '回报'])
470
  self.calculated_p_stock = calculated_p_stock
471
+ self._sycn_params()
472
  self.drawdown_plot = pn.pane.Plotly(self.plot_drawdown())
473
  super().__init__(**params)
474
 
475
+ @param.depends('select.value', watch=True)
476
+ def _sycn_params(self):
477
+ self.selected_key_column = 'cum_pnl' if self.select.value == '盈利' else 'weighted_return'
478
+
479
+ def _aggregate_by_sum(self):
480
+ # calculate weighted return
481
+ processed_df = processing.calculate_weighted_return(
482
+ self.calculated_p_stock)
483
+
484
+ agg_df = processed_df.groupby('time').aggregate({
485
+ 'weighted_return': 'sum',
486
+ 'cash': 'sum',
487
+ 'pnl': 'sum',
488
+ })
489
+
490
+ # calcualte cum pnl
491
+ agg_df['cum_pnl'] = agg_df['pnl'].cumsum()
492
+
493
+ return agg_df
494
+
495
  def calculate_drawdown(self):
496
+ agg_df = self._aggregate_by_sum()
497
+ df = processing.calculate_draw_down_on(
498
+ agg_df, self.selected_key_column)
499
+ df.reset_index(inplace=True)
 
 
 
500
  return df
501
 
502
  def plot_drawdown(self):
503
  df = self.calculate_drawdown()
504
+ fig = px.line(df, x='time', y=['drawn_down'])
505
+
506
  # add scatter to represetn new high
507
+ new_height_pnl = df[df[self.selected_key_column] ==
508
+ df[f'rolling_max_{self.selected_key_column}']]
509
+ fig.add_trace(go.Scatter(x=new_height_pnl['time'],
510
+ y=new_height_pnl['drawn_down'], mode='markers', name='新的最高总回报'))
511
  colname_to_name = {
512
  'drawn_down': '回撤'
513
  }
 
520
  ))
521
  return fig
522
 
523
+ @param.depends('selected_key_column', watch=True)
524
  def update(self):
525
+ self.drawdown_plot.object = self.plot_drawdown().to_dict()
526
 
527
  def __panel__(self):
528
+ self._layout = pn.Card(
529
+ self.select,
530
+ self.drawdown_plot,
531
+ header=pn.Row(pn.pane.Str('回撤分析')),
532
+ width=500
533
+ )
534
  return self._layout
535
 
536
 
 
540
  calculated_b_stock = param.Parameterized()
541
  calculated_p_stock = param.Parameterized()
542
  select_resolution = param.ObjectSelector(
543
+ default='每周回报', objects=['每日回报', '每周回报', '每月回报', '每年回报'])
544
 
545
  def _calculate_return(self, df, freq):
546
  # start on tuesday, end on monday
 
589
  hovertemplate=t.hovertemplate.replace(
590
  t.name, colname_to_name.get(t.name, t.name))
591
  ))
592
+
593
  fig.update_layout(barmode='group', title='主动回报归因',
594
  bargap=0.0, bargroupgap=0.0)
595
  fig.update_layout(**styling.plot_layout)
 
605
  )
606
  # update legend
607
  colname_to_name = {
608
+ 'return_p': 'portfolio回报率',
609
+ 'return_b': 'benchmark回报率'
610
  }
611
  fig.for_each_trace(lambda t: t.update(name=colname_to_name.get(t.name, t.name),
612
  legendgroup=colname_to_name.get(
 
638
  freq = 'Y'
639
  elif self.select_resolution == "每周回报":
640
  freq = 'W-MON'
641
+ agg_p = processing.aggregate_analytic_df_by_period(
642
+ self.calculated_p_stock, freq)
643
+ agg_b = processing.aggregate_analytic_df_by_period(
644
+ self.calculated_b_stock, freq)
645
  bhb_df = processing.calculate_periodic_BHB(agg_p, agg_b)
646
  agg_bhb = processing.aggregate_bhb_df(bhb_df)
647
  agg_bhb['period_str'] = agg_bhb.index.map(lambda x: str(x))
648
  return agg_bhb
649
+
650
  def __init__(self, calculated_p_stock, calculated_b_stock, **params):
651
  self.calculated_p_stock = calculated_p_stock
652
  self.calculated_b_stock = calculated_b_stock
index_page.py CHANGED
@@ -42,12 +42,12 @@ composation_card = appComponents.PortfolioComposationCard(
42
  analytic_p)
43
  monthly_return_card = appComponents.HistReturnCard(
44
  calculated_p_stock=analytic_p, calculated_b_stock=analytic_b)
45
- # total_return_card = appComponents.TotalReturnCard(name='Range', eval_df=p_eval_df,
46
- # b_stock_df=calculated_b_stock,
47
- # p_stock_df=calculated_p_stock,
48
- # value=(0, 20))
49
- # drawdown_card = appComponents.DrawDownCard(
50
- # eval_df=p_eval_df, calculated_p_stock=calculated_p_stock, calculated_b_stock=calculated_b_stock)
51
 
52
  # top_header = appComponents.TopHeader(
53
  # eval_df=p_eval_df
@@ -57,7 +57,7 @@ template = pn.template.FastListTemplate(
57
  title="Portfolio一览",
58
  # sidebar=[freq, phase],
59
  )
60
- template.main.extend([stock_overview, composation_card, monthly_return_card])
61
  # template.main.extend(
62
  # [pn.Row(top_header),
63
  # pn.Row(
 
42
  analytic_p)
43
  monthly_return_card = appComponents.HistReturnCard(
44
  calculated_p_stock=analytic_p, calculated_b_stock=analytic_b)
45
+ total_return_card = appComponents.TotalReturnCard(name='Range',
46
+ b_stock_df=analytic_b,
47
+ p_stock_df=analytic_p,
48
+ value=(0, 20))
49
+ drawdown_card = appComponents.DrawDownCard(
50
+ calculated_p_stock=analytic_p)
51
 
52
  # top_header = appComponents.TopHeader(
53
  # eval_df=p_eval_df
 
57
  title="Portfolio一览",
58
  # sidebar=[freq, phase],
59
  )
60
+ template.main.extend([drawdown_card, stock_overview, composation_card, monthly_return_card, total_return_card])
61
  # template.main.extend(
62
  # [pn.Row(top_header),
63
  # pn.Row(
instance/local.db CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:b918cd8420f4f314aaff25fb9348f5fcca206b01a0403d9a96b0039d00b55047
3
- size 17780736
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5e731caeff97539825bee38f510c68ccf49001da6f3fed73058ea4ba7891efdc
3
+ size 19148800
instance/log.json CHANGED
@@ -1,3 +1,3 @@
1
  {
2
- "daily_update": "2023-08-30 09:02:54"
3
  }
 
1
  {
2
+ "daily_update": "2023-08-31 19:34:56"
3
  }
pipeline.py CHANGED
@@ -407,7 +407,7 @@ def batch_processing():
407
  # pnl
408
  processing.calculate_pnl(analytic_p)
409
  # log return
410
- # need to crop on left side first
411
  analytic_b = analytic_b[analytic_b['time'] >= analytic_p.time.min()].copy()
412
  processing.calculate_log_return(analytic_p)
413
  processing.calculate_log_return(analytic_b)
 
407
  # pnl
408
  processing.calculate_pnl(analytic_p)
409
  # log return
410
+ # need to crop on left side of benchmark
411
  analytic_b = analytic_b[analytic_b['time'] >= analytic_p.time.min()].copy()
412
  processing.calculate_log_return(analytic_p)
413
  processing.calculate_log_return(analytic_b)
processing.py CHANGED
@@ -300,12 +300,17 @@ def calcualte_return(df: pd.DataFrame, start, end):
300
  return df
301
 
302
 
303
- def calculate_weighted_return(df: pd.DataFrame, start, end):
304
  '''
305
  calcualte weighted return within a window for each entry of ticker
306
  inclusive
307
  calculation using the weighted_log_return
308
  '''
 
 
 
 
 
309
  df = df[(df.time >= start) & (df.time <= end)].copy()
310
  inter_df = df.sort_values(by=['time'])
311
  inter_df['cum_weighted_log_return'] = inter_df.groupby(
@@ -643,19 +648,20 @@ def calculate_periodic_BHB(agg_b, agg_p):
643
  how='outer',
644
  on=['period', 'ticker'],
645
  suffixes=('_b', '_p'))
646
- merged_df['in_portfolio'].fillna(False, inplace=True)
647
- merged_df['in_benchmark'].fillna(False, inplace=True)
648
  merged_df[columns_to_fill] = merged_df[columns_to_fill].fillna(0)
649
 
650
  # complement fill aggregate_sector and display_name
651
- merged_df['aggregate_sector_b'].fillna(
652
- merged_df['aggregate_sector_p'], inplace=True)
653
- merged_df["display_name_b"].fillna(merged_df.display_name_p, inplace=True)
654
- merged_df.rename(columns={'aggregate_sector_b': 'aggregate_sector',
655
- 'display_name_b': 'display_name',
656
- }, inplace=True)
657
- merged_df.drop(columns=['aggregate_sector_p',
658
- 'display_name_p'], inplace=True)
 
 
 
659
 
660
  # calculate active return
661
  merged_df['weighted_return_p'] = merged_df['return_p'] * \
@@ -677,9 +683,34 @@ def calculate_periodic_BHB(agg_b, agg_p):
677
  return merged_df
678
 
679
 
680
- def _merge_anlaytic_df(portfolio_df, benchmark_df):
681
- pass
 
682
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
 
684
  def aggregate_analytic_df_by_period(df, freq):
685
  '''
@@ -747,3 +778,38 @@ def aggregate_bhb_df(df, by="total"):
747
  'selection',
748
  'notional_active_return']].sum()
749
  return agg_df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  return df
301
 
302
 
303
+ def calculate_weighted_return(df: pd.DataFrame, start=None, end=None):
304
  '''
305
  calcualte weighted return within a window for each entry of ticker
306
  inclusive
307
  calculation using the weighted_log_return
308
  '''
309
+ if start is None:
310
+ start = df.time.min()
311
+ if end is None:
312
+ end = df.time.max()
313
+
314
  df = df[(df.time >= start) & (df.time <= end)].copy()
315
  inter_df = df.sort_values(by=['time'])
316
  inter_df['cum_weighted_log_return'] = inter_df.groupby(
 
648
  how='outer',
649
  on=['period', 'ticker'],
650
  suffixes=('_b', '_p'))
 
 
651
  merged_df[columns_to_fill] = merged_df[columns_to_fill].fillna(0)
652
 
653
  # complement fill aggregate_sector and display_name
654
+ post_process_merged_analytic_df(merged_df)
655
+ # merged_df['in_portfolio'].fillna(False, inplace=True)
656
+ # merged_df['in_benchmark'].fillna(False, inplace=True)
657
+ # merged_df['aggregate_sector_b'].fillna(
658
+ # merged_df['aggregate_sector_p'], inplace=True)
659
+ # merged_df["display_name_b"].fillna(merged_df.display_name_p, inplace=True)
660
+ # merged_df.rename(columns={'aggregate_sector_b': 'aggregate_sector',
661
+ # 'display_name_b': 'display_name',
662
+ # }, inplace=True)
663
+ # merged_df.drop(columns=['aggregate_sector_p',
664
+ # 'display_name_p'], inplace=True)
665
 
666
  # calculate active return
667
  merged_df['weighted_return_p'] = merged_df['return_p'] * \
 
683
  return merged_df
684
 
685
 
686
+ def post_process_merged_analytic_df(merged_df):
687
+ '''
688
+ fill nan in some column on merged analytic_df
689
 
690
+ patch aggregate_sector, display_name, in_portfolio, in_benchmark,
691
+
692
+ '''
693
+ # merge both
694
+ merged_df['in_portfolio'].fillna(False, inplace=True)
695
+ merged_df['in_benchmark'].fillna(False, inplace=True)
696
+ # complement fill aggregate_sector and display_name
697
+ merged_df['aggregate_sector_b'].fillna(
698
+ merged_df['aggregate_sector_p'], inplace=True)
699
+ merged_df["display_name_b"].fillna(merged_df.display_name_p, inplace=True)
700
+ merged_df.rename(columns={'aggregate_sector_b': 'aggregate_sector',
701
+ 'display_name_b': 'display_name',
702
+ }, inplace=True)
703
+ merged_df.drop(columns=['aggregate_sector_p',
704
+ 'display_name_p'], inplace=True)
705
+
706
+
707
+ def calculate_weighted_pct(df):
708
+ '''
709
+ patch df with weighted pct, if pct is not calculated patch that as well
710
+ '''
711
+ if 'pct' not in df.columns:
712
+ calculate_pct(df)
713
+ df['weighted_pct'] = df['pct'] * df['weight']
714
 
715
  def aggregate_analytic_df_by_period(df, freq):
716
  '''
 
778
  'selection',
779
  'notional_active_return']].sum()
780
  return agg_df
781
+
782
+ def calculate_draw_down_on(df, key='weighted_return'):
783
+ '''
784
+ calculate draw down on anlaytic df based on either return or accumulative pnl
785
+
786
+ Parameters
787
+ ----------
788
+ df : pd.DataFrame
789
+ analytic df
790
+ key : str, optional
791
+ cum_pnl or weighted_return, by default 'weighted_return'
792
+ '''
793
+ if key not in df.columns:
794
+ raise ValueError(f'{key} not in df')
795
+ else:
796
+ df = df.sort_values(by=['time'])
797
+ df[f'rolling_max_{key}'] = df[key].rolling(
798
+ window=len(df), min_periods=1).max()
799
+ if key == 'pnl':
800
+ df['drawn_down'] = df[key] / df[f'rolling_max_{key}']
801
+
802
+ else:
803
+ df['drawn_down'] = (1 + df[key]) / (1 + df[f'rolling_max_{key}'])
804
+
805
+ return df
806
+
807
+ # def calculate_accumulative_pnl(df):
808
+ # '''
809
+ # calculate accumulative pnl on analytic df
810
+ # '''
811
+ # df = df.sort_values(by=['time'])
812
+ # df['accumulative_pnl'] = df.groupby('ticker')['pnl'].rolling(
813
+
814
+ # )
815
+ # return df
utils.py CHANGED
@@ -140,3 +140,75 @@ def create_share_changes_report(df):
140
  markdown += '{} | {} | {}\n'.format(row['ticker'],
141
  row['display_name'], share_changes_str)
142
  return markdown
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  markdown += '{} | {} | {}\n'.format(row['ticker'],
141
  row['display_name'], share_changes_str)
142
  return markdown
143
+
144
+
145
+ def create_html_report(result: list[tuple]):
146
+ '''
147
+ a flex box with 2 flex item on each row where justified space-between
148
+
149
+ Parameters
150
+ ----------
151
+ result: list of tuple
152
+ (title, value, type)
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
160
+ '''
161
+ style = '''
162
+ <style>
163
+ .compact-container {{
164
+ display: flex;
165
+ flex-direction: column;
166
+ gap: 5px;
167
+ }}
168
+
169
+ .compact-container > div {{
170
+ display: flex;
171
+ justify-content: space-between;
172
+ margin-bottom: 2px;
173
+ }}
174
+
175
+ .compact-container > div > h2,
176
+ .compact-container > div > h3,
177
+ .compact-container > div > p,
178
+ .compact-container > div > ul > li {{
179
+ margin: 0;
180
+ }}
181
+
182
+ .compact-container > ul {{
183
+ padding: 0;
184
+ margin: 0;
185
+ list-style-type: none;
186
+ }}
187
+
188
+ .compact-container > ul > li {{
189
+ display: flex;
190
+ margin-bottom: 2px;
191
+ }}
192
+ </style>
193
+ '''
194
+
195
+ def _get_color(num):
196
+ return 'green' if num >= 0 else 'red'
197
+
198
+ def _format_percentage_number(num):
199
+ return f'{round(num * 100, 2)}%'
200
+
201
+ def _create_flex_item(result_entry):
202
+ key, value, value_type = result_entry
203
+ return f"""
204
+ <div>
205
+ <p style="margin: 0;">{key}</p>
206
+ <p style='color: {_get_color(value)}; margin: 0;'>{_format_percentage_number(value)}</p>
207
+ </div>
208
+ """
209
+ html = f"""
210
+ <div class="compact-container">
211
+ {''.join([_create_flex_item(entry) for entry in result])}
212
+ </div>
213
+ """
214
+ return style + html