Spaces:
Runtime error
Runtime error
huggingface112
commited on
Commit
•
b9d37d3
1
Parent(s):
a77544c
migrate drawDownCard to new pipeline
Browse files- .editorconfig +59 -0
- appComponents.py +151 -76
- index_page.py +7 -7
- instance/local.db +2 -2
- instance/log.json +1 -1
- pipeline.py +1 -1
- processing.py +79 -13
- 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 |
-
|
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
|
235 |
-
|
236 |
-
|
237 |
-
most_recent_row = result.
|
238 |
-
active_return = most_recent_row.active_return
|
239 |
-
tracking_error =
|
240 |
-
total_return = most_recent_row.
|
241 |
-
mkt_cap = most_recent_row.
|
242 |
-
risk =
|
243 |
|
244 |
# Calculate the total attribution
|
245 |
-
attributes = processing.calculate_attributes_between_dates(
|
246 |
-
|
247 |
-
total_attributes = attributes.aggregate({
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
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 |
-
|
350 |
-
|
351 |
-
|
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 |
-
'
|
357 |
-
'
|
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', '
|
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,
|
376 |
-
|
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=
|
381 |
-
end=
|
382 |
-
value=(
|
|
|
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('
|
401 |
-
def
|
402 |
-
|
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 |
-
|
418 |
-
|
|
|
|
|
|
|
|
|
419 |
self.calculated_p_stock = calculated_p_stock
|
420 |
-
self.
|
421 |
self.drawdown_plot = pn.pane.Plotly(self.plot_drawdown())
|
422 |
super().__init__(**params)
|
423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
424 |
def calculate_drawdown(self):
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
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=
|
|
|
437 |
# add scatter to represetn new high
|
438 |
-
new_height_pnl = df[df.
|
439 |
-
|
440 |
-
|
|
|
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 |
-
|
455 |
|
456 |
def __panel__(self):
|
457 |
-
self._layout = pn.Card(
|
458 |
-
|
459 |
-
|
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='
|
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 |
-
'
|
536 |
-
'
|
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(
|
569 |
-
|
|
|
|
|
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 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
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:
|
3 |
-
size
|
|
|
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-
|
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
|
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
|
652 |
-
|
653 |
-
merged_df[
|
654 |
-
merged_df
|
655 |
-
|
656 |
-
|
657 |
-
merged_df.
|
658 |
-
|
|
|
|
|
|
|
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
|
681 |
-
|
|
|
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
|