wbreport 2.2.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wbreport might be problematic. Click here for more details.

Files changed (66) hide show
  1. wbreport/__init__.py +1 -0
  2. wbreport/admin.py +87 -0
  3. wbreport/apps.py +6 -0
  4. wbreport/defaults/__init__.py +0 -0
  5. wbreport/defaults/factsheets/__init__.py +0 -0
  6. wbreport/defaults/factsheets/base.py +990 -0
  7. wbreport/defaults/factsheets/menu.py +93 -0
  8. wbreport/defaults/factsheets/mixins.py +35 -0
  9. wbreport/defaults/factsheets/multitheme.py +947 -0
  10. wbreport/dynamic_preferences_registry.py +15 -0
  11. wbreport/factories/__init__.py +8 -0
  12. wbreport/factories/data_classes.py +48 -0
  13. wbreport/factories/reports.py +79 -0
  14. wbreport/filters.py +37 -0
  15. wbreport/migrations/0001_initial_squashed_squashed_0007_report_key.py +238 -0
  16. wbreport/migrations/0008_alter_report_file_content_type.py +25 -0
  17. wbreport/migrations/0009_alter_report_color_palette.py +27 -0
  18. wbreport/migrations/0010_auto_20240103_0947.py +43 -0
  19. wbreport/migrations/0011_auto_20240207_1629.py +35 -0
  20. wbreport/migrations/0012_reportversion_lock.py +17 -0
  21. wbreport/migrations/0013_alter_reportversion_context.py +18 -0
  22. wbreport/migrations/0014_alter_reportcategory_options_and_more.py +25 -0
  23. wbreport/migrations/__init__.py +0 -0
  24. wbreport/mixins.py +183 -0
  25. wbreport/models.py +781 -0
  26. wbreport/pdf/__init__.py +0 -0
  27. wbreport/pdf/charts/__init__.py +0 -0
  28. wbreport/pdf/charts/legend.py +15 -0
  29. wbreport/pdf/charts/pie.py +169 -0
  30. wbreport/pdf/charts/timeseries.py +77 -0
  31. wbreport/pdf/sandbox/__init__.py +0 -0
  32. wbreport/pdf/sandbox/run.py +17 -0
  33. wbreport/pdf/sandbox/templates/__init__.py +0 -0
  34. wbreport/pdf/sandbox/templates/basic_factsheet.py +908 -0
  35. wbreport/pdf/sandbox/templates/fund_factsheet.py +864 -0
  36. wbreport/pdf/sandbox/templates/long_industry_exposure_factsheet.py +898 -0
  37. wbreport/pdf/sandbox/templates/multistrat_factsheet.py +872 -0
  38. wbreport/pdf/sandbox/templates/testfile.pdf +434 -0
  39. wbreport/pdf/tables/__init__.py +0 -0
  40. wbreport/pdf/tables/aggregated_tables.py +156 -0
  41. wbreport/pdf/tables/data_tables.py +75 -0
  42. wbreport/serializers.py +191 -0
  43. wbreport/tasks.py +60 -0
  44. wbreport/templates/__init__.py +0 -0
  45. wbreport/templatetags/__init__.py +0 -0
  46. wbreport/templatetags/portfolio_tags.py +35 -0
  47. wbreport/tests/__init__.py +0 -0
  48. wbreport/tests/conftest.py +24 -0
  49. wbreport/tests/test_models.py +253 -0
  50. wbreport/tests/test_tasks.py +17 -0
  51. wbreport/tests/test_viewsets.py +0 -0
  52. wbreport/tests/tests.py +12 -0
  53. wbreport/urls.py +29 -0
  54. wbreport/urls_public.py +10 -0
  55. wbreport/viewsets/__init__.py +10 -0
  56. wbreport/viewsets/configs/__init__.py +18 -0
  57. wbreport/viewsets/configs/buttons.py +193 -0
  58. wbreport/viewsets/configs/displays.py +116 -0
  59. wbreport/viewsets/configs/endpoints.py +23 -0
  60. wbreport/viewsets/configs/menus.py +8 -0
  61. wbreport/viewsets/configs/titles.py +30 -0
  62. wbreport/viewsets/viewsets.py +330 -0
  63. wbreport-2.2.1.dist-info/METADATA +6 -0
  64. wbreport-2.2.1.dist-info/RECORD +66 -0
  65. wbreport-2.2.1.dist-info/WHEEL +5 -0
  66. wbreport-2.2.1.dist-info/licenses/LICENSE +4 -0
@@ -0,0 +1,947 @@
1
+ from datetime import date
2
+ from decimal import Decimal
3
+ from io import BytesIO
4
+
5
+ import pandas as pd
6
+ from django.template.loader import get_template
7
+ from reportlab import platypus
8
+ from reportlab.graphics.charts.barcharts import HorizontalBarChart
9
+ from reportlab.graphics.shapes import Drawing, String
10
+ from reportlab.lib import colors
11
+ from reportlab.lib.colors import HexColor
12
+ from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_RIGHT
13
+ from reportlab.lib.formatters import DecimalFormatter
14
+ from reportlab.lib.pagesizes import A4
15
+ from reportlab.lib.styles import ParagraphStyle
16
+ from reportlab.lib.units import cm
17
+ from reportlab.pdfbase import pdfmetrics
18
+ from reportlab.pdfbase.pdfmetrics import registerFontFamily, stringWidth
19
+ from reportlab.pdfbase.ttfonts import TTFont
20
+ from reportlab.platypus import (
21
+ BaseDocTemplate,
22
+ NextPageTemplate,
23
+ PageTemplate,
24
+ Paragraph,
25
+ Spacer,
26
+ Table,
27
+ TableStyle,
28
+ )
29
+ from reportlab.platypus.frames import Frame
30
+ from wbcore.utils.figures import (
31
+ get_factsheet_timeseries_chart,
32
+ get_horizontal_barplot,
33
+ get_piechart,
34
+ )
35
+ from wbportfolio.models.products import InvestmentIndex
36
+ from wbreport.mixins import ReportMixin
37
+ from wbreport.models import ReportAsset
38
+ from wbreport.pdf.charts.pie import (
39
+ get_pie_chart_horizontal,
40
+ get_pie_chart_horizontal_height,
41
+ )
42
+ from wbreport.pdf.charts.timeseries import Scale, get_timeseries_chart
43
+ from wbreport.pdf.flowables.risk import RiskScale
44
+ from wbreport.pdf.flowables.textboxes import TextWithIcon
45
+ from wbreport.pdf.flowables.themes import ThemeBreakdown
46
+ from wbreport.pdf.tables.aggregated_tables import get_simple_aggregated_table
47
+ from wbreport.pdf.tables.data_tables import get_simple_data_table
48
+
49
+ from .mixins import FactsheetReportMixin
50
+
51
+
52
+ class ReportClass(FactsheetReportMixin, ReportMixin):
53
+ HTML_TEMPLATE_FILE = "report/factsheet_multitheme.html"
54
+
55
+ @classmethod
56
+ def get_context(cls, version):
57
+ content_object = version.report.content_object
58
+ parameters = cls.parse_parameters(version.parameters)
59
+
60
+ asset_portfolio = content_object.asset_portfolio
61
+ base_portfolio = content_object.primary_portfolio
62
+ context = {"product_title": content_object.title}
63
+
64
+ end = content_object.asset_portfolio.get_latest_asset_position_date(parameters["end"])
65
+ start = content_object.asset_portfolio.get_latest_asset_position_date(parameters["start"])
66
+ prices = content_object.get_prices_df(from_date=end)
67
+ if start and end and not prices.empty:
68
+ context["date"] = end
69
+
70
+ if content_object.group:
71
+ context["funds_table"] = content_object.group.get_fund_product_table(end)
72
+
73
+ context["currency"] = content_object.currency.symbol
74
+ context["external_webpage"] = content_object.external_webpage
75
+ context["information_table"] = content_object.get_factsheet_table_data(end)
76
+ context["monthly_returns"] = content_object.get_monthly_return_summary_dict(end=end)
77
+
78
+ context["introduction"] = content_object.description
79
+ context["all_time"] = content_object.get_price_range(val_date=end)
80
+ context["risk_scale"] = content_object.risk_scale
81
+ context["risk_scale_loop"] = [True] * content_object.risk_scale + [False] * (7 - content_object.risk_scale)
82
+ context["holdings"] = asset_portfolio.get_holding(end).values(
83
+ "underlying_instrument__name_repr", "weighting"
84
+ )
85
+ contributors = (
86
+ asset_portfolio.get_portfolio_contribution_df(start, end, with_cash=False)
87
+ .sort_values(by="contribution_total", ascending=False)
88
+ .underlying_instrument__name_repr
89
+ )
90
+
91
+ if contributors is not None and not contributors.empty:
92
+ contributors_list = contributors.values.tolist()
93
+ context["top_contributors"] = contributors_list[:3]
94
+ contributors_list.reverse()
95
+ context["worst_contributors"] = contributors_list[:3]
96
+
97
+ context["prices"] = prices
98
+ context["geographical_breakdown"] = asset_portfolio.get_geographical_breakdown(end)
99
+ context["currency_exposure"] = asset_portfolio.get_currency_exposure(end)
100
+ context["asset_allocation"] = asset_portfolio.get_asset_allocation(end)
101
+ context["market_cap_distribution"] = asset_portfolio.get_equity_market_cap_distribution(end)
102
+ context["equity_liquidity"] = asset_portfolio.get_equity_liquidity(end)
103
+ context["industry_exposure"] = asset_portfolio.get_industry_exposure(end).sort_values(
104
+ by=["weighting"], ascending=False
105
+ )
106
+ if content_object.investment_index == InvestmentIndex.LONG_SHORT.name:
107
+ context["longshort"] = base_portfolio.get_longshort_distribution(end)
108
+ else:
109
+ raise ValueError("Context cannot be generated")
110
+ return context
111
+
112
+ @classmethod
113
+ def generate_html(cls, context):
114
+ if "funds_table" in context:
115
+ context["funds_table"] = context["funds_table"].to_html(
116
+ border=0,
117
+ index=False,
118
+ classes=["table", "table-funds", "table-colored"],
119
+ index_names=False,
120
+ float_format="%.1f%%",
121
+ na_rep="",
122
+ )
123
+
124
+ context["prices"] = get_factsheet_timeseries_chart(
125
+ context["prices"], color=context["report_base_color"]
126
+ ).to_html(full_html=False, include_plotlyjs=False)
127
+
128
+ nb_labels_first_row = max(
129
+ [
130
+ context["geographical_breakdown"].shape[0],
131
+ context["currency_exposure"].shape[0],
132
+ context["asset_allocation"].shape[0],
133
+ ]
134
+ )
135
+ height_piechart = 300
136
+ context["geographical_breakdown"] = get_piechart(
137
+ context["geographical_breakdown"],
138
+ colors_map=context["colors_palette"],
139
+ height=height_piechart,
140
+ max_number_label=nb_labels_first_row,
141
+ ).to_html(full_html=False, include_plotlyjs=False)
142
+ context["currency_exposure"] = get_piechart(
143
+ context["currency_exposure"],
144
+ colors_map=context["colors_palette"],
145
+ height=height_piechart,
146
+ max_number_label=nb_labels_first_row,
147
+ ).to_html(full_html=False, include_plotlyjs=False)
148
+ context["asset_allocation"] = get_piechart(
149
+ context["asset_allocation"],
150
+ colors_map=context["colors_palette"],
151
+ height=height_piechart,
152
+ max_number_label=nb_labels_first_row,
153
+ ).to_html(full_html=False, include_plotlyjs=False)
154
+
155
+ # SECOND WWIDGET ROW
156
+ context["market_cap_distribution"] = get_piechart(
157
+ context["market_cap_distribution"], height=height_piechart, colors_map=context["colors_palette"]
158
+ ).to_html(full_html=False, include_plotlyjs=False)
159
+ context["equity_liquidity"] = get_piechart(
160
+ context["equity_liquidity"], height=height_piechart, colors_map=context["colors_palette"]
161
+ ).to_html(full_html=False, include_plotlyjs=False)
162
+
163
+ context["industry_exposure"] = get_horizontal_barplot(
164
+ context["industry_exposure"], colors=context["colors_palette"][0]
165
+ ).to_html(full_html=False, include_plotlyjs=False)
166
+ if "strategy_allocation" in context:
167
+ index_allocation_df = context["strategy_allocation"]
168
+ if not index_allocation_df.empty:
169
+ context["strategy_allocation"] = get_piechart(
170
+ index_allocation_df,
171
+ x_label="allocation_end",
172
+ height=height_piechart,
173
+ y_label="underlying_instrument__name_repr",
174
+ hoverinfo="text",
175
+ colors_label="color",
176
+ ).to_html(full_html=False, include_plotlyjs=False)
177
+ renamed_df = index_allocation_df.rename(
178
+ columns={"performance_total": "Monthly Performance", "contribution_total": "Monthly Contribution"}
179
+ )
180
+ context["stragegy_contribution"] = get_horizontal_barplot(
181
+ renamed_df,
182
+ colors_label="color",
183
+ x_label=["Monthly Performance", "Monthly Contribution"],
184
+ y_label="underlying_instrument__name_repr",
185
+ ).to_html(full_html=False, include_plotlyjs=False)
186
+
187
+ if not context.get("longshort", pd.DataFrame()).empty:
188
+ context["longshort"] = get_piechart(
189
+ context["longshort"],
190
+ colors_map=context["colors_palette"],
191
+ hoverinfo="label+text",
192
+ default_normalize=False,
193
+ height=height_piechart,
194
+ y_label="title",
195
+ ).to_html(full_html=False, include_plotlyjs=False)
196
+
197
+ template = get_template(cls.HTML_TEMPLATE_FILE)
198
+ return template.render(context)
199
+
200
+ @classmethod
201
+ def generate_file(cls, context):
202
+ debug = False
203
+
204
+ main_features_dict = context["information_table"]["Main Features"]
205
+ # Monthly returns table as dataframe, None is no value
206
+ monthly_returns = context["monthly_returns"]
207
+
208
+ # Pie chart Dataframe
209
+ geographical = context["geographical_breakdown"]
210
+ currencies = context["currency_exposure"]
211
+ allocation = context["asset_allocation"]
212
+ marketcap = context["market_cap_distribution"]
213
+ liquidity = context["equity_liquidity"]
214
+ industry = context["industry_exposure"]
215
+
216
+ # Price time serie as dataframe
217
+ prices = context["prices"]
218
+
219
+ # TOPs as list
220
+
221
+ top_3_holdings = list(context["holdings"].values_list("underlying_instrument__name_repr", flat=True))[0:3]
222
+ top_3_holdings = [t.upper() for t in top_3_holdings]
223
+
224
+ top_3_contributors = [c.upper() for c in context["top_contributors"]]
225
+ bottom_3_contributors = [c.upper() for c in context["worst_contributors"]]
226
+
227
+ # {'high': {'price':low, 'date':date}, 'low' : {'price':high, 'date':date}}
228
+ all_time_dict = context["all_time"]
229
+ end = context["date"]
230
+ general_data = {}
231
+ general_data["date"] = end.strftime("%b %Y")
232
+ general_data["colors"] = context["colors_palette"]
233
+ general_data["title"] = context["product_title"].replace("&", "&")
234
+ general_data["risk_scale"] = context["risk_scale"]
235
+
236
+ general_data["introduction"] = context["introduction"]
237
+ theme_breakdown_df = context["strategy_allocation"]
238
+
239
+ # Register Fonts
240
+ pdfmetrics.registerFont(TTFont("customfont", ReportAsset.objects.get(key="font-default").asset))
241
+ pdfmetrics.registerFont(TTFont("customfont-bd", ReportAsset.objects.get(key="font-bd").asset))
242
+ pdfmetrics.registerFont(TTFont("customfont-it", ReportAsset.objects.get(key="font-it").asset))
243
+ registerFontFamily("customfont", normal="customfont", bold="customfont-bd", italic="customfont-it")
244
+
245
+ # Page Variables
246
+ LINEHEIGHT = 12
247
+
248
+ HEADER_HEIGHT = 2.34 * cm
249
+ FOOTER_HEIGHT = 2.34 * cm
250
+
251
+ SIDE_MARGIN = 0.96 * cm
252
+ TOP_MARGIN = 3.63 * cm
253
+ CONTENT_OFFSET = 0.34 * cm
254
+ CONTENT_MARGIN = SIDE_MARGIN + CONTENT_OFFSET
255
+
256
+ CONTENT_HEIGHT = 22.25 * cm
257
+ CONTENT_WIDTH_PAGE1_LEFT = 13.84 * cm
258
+ CONTENT_WIDTH_PAGE1_RIGHT = 4.23 * cm
259
+ CONTENT_WIDTH_PAGE2 = A4[0] - SIDE_MARGIN - CONTENT_MARGIN
260
+
261
+ CONTENT_X_PAGE1_RIGHT = A4[0] - CONTENT_MARGIN - CONTENT_WIDTH_PAGE1_RIGHT
262
+ CONTENT_Y = A4[1] - CONTENT_HEIGHT - TOP_MARGIN
263
+
264
+ TABLE_MARGIN_PAGE2 = 0.56 * cm
265
+
266
+ output = BytesIO()
267
+ doc = BaseDocTemplate(
268
+ output,
269
+ pagesize=A4,
270
+ rightMargin=0,
271
+ leftMargin=0,
272
+ topMargin=0,
273
+ bottomMargin=0,
274
+ title=general_data["title"],
275
+ author="Stainly",
276
+ )
277
+ elements = []
278
+
279
+ s_base = ParagraphStyle(name="s_base", fontName="customfont", fontSize=9, leading=10)
280
+ s_base_small_justified = ParagraphStyle(
281
+ name="s_base_small_justified", parent=s_base, fontSize=6.5, leading=7, alignment=TA_JUSTIFY
282
+ )
283
+ # s_base_small_justified_indent = ParagraphStyle(
284
+ # name="s_base_small_justified_indent", parent=s_base_small_justified, leftIndent=CONTENT_OFFSET
285
+ # )
286
+ s_base_indent = ParagraphStyle(name="s_description", parent=s_base, spaceBefore=8, leftIndent=CONTENT_OFFSET)
287
+ s_table_base = ParagraphStyle(
288
+ name="s_table_base",
289
+ fontName="customfont",
290
+ fontSize=6,
291
+ leading=6,
292
+ )
293
+ s_table_medium = ParagraphStyle(name="s_table_medium", parent=s_table_base, fontSize=9, leading=8)
294
+ s_table_medium_leading = ParagraphStyle(name="s_table_medium_leading", parent=s_table_medium, leading=13.9)
295
+ # s_table_large = ParagraphStyle(name="s_table_large", parent=s_table_medium, fontSize=11, leading=11)
296
+ # s_table_large_center = ParagraphStyle(name="s_table_large", parent=s_table_large, alignment=TA_CENTER)
297
+ # s_table_large_center_padding = ParagraphStyle(
298
+ # name="s_table_large", parent=s_table_large_center, spaceBefore=20, spaceAfter=20
299
+ # )
300
+ # s_table_center = ParagraphStyle(
301
+ # name="s_table_center",
302
+ # parent=s_table_base,
303
+ # alignment=TA_CENTER,
304
+ # )
305
+ # s_table_center_high = ParagraphStyle(name="s_table_center", parent=s_table_center, leading=9, fontSize=8)
306
+ s_table_right = ParagraphStyle(name="s_table_right", parent=s_table_base, alignment=TA_RIGHT)
307
+ s_table_center = ParagraphStyle(name="s_table_center", parent=s_table_base, alignment=TA_CENTER)
308
+ s_table_headline = ParagraphStyle(
309
+ name="s_table_headline",
310
+ parent=s_table_base,
311
+ fontSize=16,
312
+ leading=16,
313
+ )
314
+ # s_table_headline_2 = ParagraphStyle(
315
+ # name="s_table_headline_2",
316
+ # parent=s_table_base,
317
+ # fontSize=10,
318
+ # leading=10,
319
+ # )
320
+
321
+ # Setup Colors
322
+ c_product = HexColor(general_data["colors"][0])
323
+ c_product_alpha = HexColor(f"{general_data['colors'][0]}20", hasAlpha=True)
324
+
325
+ c_table_border = HexColor(0x9EA3AC)
326
+ c_grid_color = HexColor(0xB6BAC1)
327
+ c_box_color = HexColor(0x3C4859)
328
+ c_table_background = colors.HexColor(0xE2E3E6)
329
+
330
+ # Frame and Page Layout
331
+ frame_defaults = {
332
+ "showBoundary": debug,
333
+ "leftPadding": 0,
334
+ "rightPadding": 0,
335
+ "topPadding": 0,
336
+ "bottomPadding": 0,
337
+ }
338
+
339
+ text_frame = Frame(
340
+ x1=SIDE_MARGIN,
341
+ y1=CONTENT_Y,
342
+ width=CONTENT_WIDTH_PAGE1_LEFT,
343
+ height=CONTENT_HEIGHT,
344
+ id="text_frame",
345
+ **frame_defaults,
346
+ )
347
+
348
+ main_features_frame = Frame(
349
+ x1=CONTENT_X_PAGE1_RIGHT,
350
+ y1=CONTENT_Y,
351
+ width=CONTENT_WIDTH_PAGE1_RIGHT,
352
+ height=CONTENT_HEIGHT,
353
+ id="main_features_frame",
354
+ **frame_defaults,
355
+ )
356
+
357
+ second_page = Frame(
358
+ x1=SIDE_MARGIN,
359
+ y1=CONTENT_Y,
360
+ width=CONTENT_WIDTH_PAGE2,
361
+ height=CONTENT_HEIGHT,
362
+ id="second_page",
363
+ **frame_defaults,
364
+ )
365
+
366
+ def on_page(canv, dock):
367
+ canv.saveState()
368
+
369
+ # Header
370
+ canv.setFillColor(c_box_color)
371
+ canv.rect(0, A4[1] - HEADER_HEIGHT, A4[0], HEADER_HEIGHT, fill=True, stroke=False)
372
+
373
+ colors = [
374
+ HexColor(0xFFB166),
375
+ HexColor(0x8CD66B),
376
+ HexColor(0x05D6A1),
377
+ HexColor(0x01ABAA),
378
+ HexColor(0x70D6FF),
379
+ HexColor(0x0585FF),
380
+ HexColor(0x5724D9),
381
+ HexColor(0xA359E5),
382
+ HexColor(0xEF476F),
383
+ ]
384
+ height = 0.13 * cm
385
+ width = A4[0] / len(colors)
386
+ for index, color in enumerate(colors):
387
+ canv.setFillColor(color)
388
+ canv.rect(0 + index * width, A4[1] - HEADER_HEIGHT - height, width, height, fill=True, stroke=False)
389
+
390
+ # Footer
391
+ canv.setFillColor(c_box_color)
392
+ canv.rect(0, 0, A4[0], FOOTER_HEIGHT, fill=True, stroke=False)
393
+
394
+ for index, color in enumerate(colors):
395
+ canv.setFillColor(color)
396
+ canv.rect(0 + index * width, FOOTER_HEIGHT, width, height, fill=True, stroke=False)
397
+
398
+ canv.restoreState()
399
+
400
+ doc.addPageTemplates(
401
+ [
402
+ PageTemplate(id="page", onPage=on_page, frames=[text_frame, main_features_frame]),
403
+ PageTemplate(id="second_page", onPage=on_page, frames=[second_page]),
404
+ ]
405
+ )
406
+
407
+ elements.append(NextPageTemplate(["second_page"]))
408
+
409
+ def generate_title(title):
410
+ table_data = [[Paragraph("", style=s_table_headline), Paragraph(title, style=s_table_headline)]]
411
+ title_table = Table(table_data, colWidths=[0.14 * cm, None], rowHeights=[0.41 * cm])
412
+ title_table.setStyle(
413
+ TableStyle(
414
+ [
415
+ ("BACKGROUND", (0, 0), (0, 0), c_product),
416
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
417
+ ("LEFTPADDING", (1, 0), (-1, -1), 0.2 * cm),
418
+ ("RIGHTPADDING", (0, 0), (-1, -1), 0),
419
+ ("TOPPADDING", (0, 0), (-1, -1), 0),
420
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
421
+ ]
422
+ )
423
+ )
424
+ return title_table
425
+
426
+ # Description
427
+ elements.append(generate_title(general_data["title"]))
428
+ elements.append(Paragraph(general_data["introduction"], style=s_base_indent))
429
+ elements.append(Spacer(height=LINEHEIGHT * 2, width=0))
430
+
431
+ # Monthly Returns
432
+ elements.append(generate_title("Monthly Returns (%)"))
433
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
434
+ elements.append(
435
+ get_simple_aggregated_table(
436
+ df=monthly_returns,
437
+ width=CONTENT_WIDTH_PAGE1_LEFT,
438
+ row_height=0.389 * cm,
439
+ header_style=s_table_center,
440
+ row_style=s_table_center,
441
+ data_style=s_table_right,
442
+ grid_color=c_table_border,
443
+ offset=CONTENT_OFFSET,
444
+ debug=debug,
445
+ )
446
+ )
447
+ elements.append(Spacer(height=LINEHEIGHT * 2, width=0))
448
+
449
+ # Price Timeseries Chart
450
+ elements.append(
451
+ get_timeseries_chart(
452
+ data=[list(zip(prices.index, prices.net_value))],
453
+ width=CONTENT_WIDTH_PAGE1_LEFT - CONTENT_OFFSET,
454
+ height=4.34 * cm,
455
+ color=c_product,
456
+ fill_color=c_product_alpha,
457
+ grid_color=c_grid_color,
458
+ scale=Scale.LOGARITHMIC.value,
459
+ debug=debug,
460
+ x=CONTENT_OFFSET,
461
+ )
462
+ )
463
+ elements.append(Spacer(height=LINEHEIGHT * 2, width=0))
464
+
465
+ # Top 3
466
+ elements.append(
467
+ get_simple_data_table(
468
+ headers=[
469
+ "<strong>Top 3 Holdings</strong>",
470
+ "<strong>Top 3 Contributors</strong>",
471
+ "<strong>Bottom 3 Contributors</strong>",
472
+ ],
473
+ data=list(zip(top_3_holdings, top_3_contributors, bottom_3_contributors)),
474
+ width=CONTENT_WIDTH_PAGE1_LEFT,
475
+ header_row_height=0.85 * cm,
476
+ data_row_height=0.39 * cm,
477
+ margin=0.16 * cm,
478
+ header_style=s_table_medium,
479
+ data_style=s_table_base,
480
+ grid_color=c_table_border,
481
+ offset=CONTENT_OFFSET,
482
+ debug=debug,
483
+ )
484
+ )
485
+
486
+ elements.append(platypus.FrameBreak("main_features_frame"))
487
+
488
+ # Main Features Table
489
+
490
+ MAIN_FEATURES_COLOR_BAR_HEIGHT = 0.23 * cm
491
+ MAIN_FEATURES_GAP_HEIGHT = 0.17 * cm
492
+ MAIN_FEATURES_TITLE_HEIGHT = 1.94 * cm
493
+ MAIN_FEATURES_TITLE1_HEIGHT = 0.74 * cm
494
+
495
+ table_data = [
496
+ [Spacer(width=0, height=0)],
497
+ [
498
+ TextWithIcon(
499
+ width=CONTENT_WIDTH_PAGE1_RIGHT,
500
+ height=MAIN_FEATURES_TITLE_HEIGHT,
501
+ text=general_data["date"],
502
+ font="customfont-bd",
503
+ font_size=11,
504
+ icon=context["logo_file"] if "logo_file" in context else None,
505
+ )
506
+ ],
507
+ [Paragraph("<strong>MAIN FEATURES</strong>", style=s_table_center)],
508
+ [Spacer(width=0, height=0)],
509
+ ]
510
+
511
+ for label, value in main_features_dict.items():
512
+ if isinstance(value, date):
513
+ value = value.strftime("%d-%b-%y")
514
+ elif isinstance(value, (Decimal, float)):
515
+ value = "%.2f" % value
516
+ elif isinstance(value, int):
517
+ value = str(value)
518
+ elif value is None:
519
+ value = ""
520
+
521
+ if label.lower() in ["currency", "last price"]:
522
+ table_data.append(
523
+ [
524
+ Paragraph(f"<strong>{label.upper()}</strong>", style=s_table_base),
525
+ Paragraph(f"<strong>{value.upper()}</strong>", style=s_table_base),
526
+ ]
527
+ )
528
+ else:
529
+ table_data.append(
530
+ [
531
+ Paragraph(label.upper(), style=s_table_base),
532
+ Paragraph(value.upper(), style=s_table_base),
533
+ ]
534
+ )
535
+
536
+ table_data.extend([[Spacer(width=0, height=0)], [Spacer(width=0, height=0)]])
537
+
538
+ row_heights = [
539
+ MAIN_FEATURES_COLOR_BAR_HEIGHT,
540
+ MAIN_FEATURES_TITLE_HEIGHT,
541
+ MAIN_FEATURES_TITLE1_HEIGHT,
542
+ MAIN_FEATURES_GAP_HEIGHT,
543
+ ]
544
+ row_heights.extend([None] * (len(table_data) - 6))
545
+ row_heights.extend([MAIN_FEATURES_GAP_HEIGHT, MAIN_FEATURES_COLOR_BAR_HEIGHT])
546
+
547
+ t = Table(table_data, rowHeights=row_heights, colWidths=[CONTENT_WIDTH_PAGE1_RIGHT / 2] * 2)
548
+ table_styles = [
549
+ ("BACKGROUND", (0, 0), (1, 0), c_product), # Top Color Bar
550
+ ("BACKGROUND", (0, -1), (1, -1), c_product), # Bottom Color Bar
551
+ ("BACKGROUND", (0, 2), (-1, 2), c_table_background), # Title Background
552
+ ("LINEABOVE", (0, 2), (-1, 2), 0.25, colors.HexColor("#6d7683")), # Title Line Above
553
+ ("LINEBELOW", (0, 2), (-1, 2), 0.25, colors.HexColor("#6d7683")), # Title Line Below
554
+ ("LINEBEFORE", (1, 3), (1, -2), 0.25, colors.HexColor("#6d7683")), # Data Vertical Seperator
555
+ ("SPAN", (0, 1), (-1, 1)), # Col Span Title
556
+ ("SPAN", (0, 2), (-1, 2)), # Col Span Title1
557
+ ("LEFTPADDING", (0, 1), (0, -1), 0), # Leftpadding Data Labels
558
+ ("LEFTPADDING", (1, 1), (1, -1), 0.28 * cm), # Leftpadding Data Values
559
+ ("VALIGN", (0, 1), (-1, -1), "MIDDLE"),
560
+ ]
561
+
562
+ if debug:
563
+ table_styles.extend(
564
+ [
565
+ ("INNERGRID", (0, 0), (-1, -1), 0.25, colors.black),
566
+ ("BOX", (0, 0), (-1, -1), 0.25, colors.black),
567
+ ]
568
+ )
569
+
570
+ t.setStyle(TableStyle(table_styles))
571
+
572
+ elements.append(t)
573
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
574
+
575
+ # All time table
576
+ table_data = [
577
+ [
578
+ Paragraph("<strong>ALL TIME HIGH</strong>", style=s_table_center),
579
+ Paragraph("<strong>ALL TIME LOW</strong>", style=s_table_center),
580
+ ],
581
+ [
582
+ Paragraph("", style=s_table_center),
583
+ Paragraph("", style=s_table_center),
584
+ ],
585
+ ]
586
+
587
+ table_data.append(
588
+ [
589
+ Paragraph(f'PRICE: {all_time_dict["high"]["price"]:.1f}', style=s_table_center),
590
+ Paragraph(f'PRICE: {all_time_dict["low"]["price"]:.1f}', style=s_table_center),
591
+ ],
592
+ )
593
+ table_data.append(
594
+ [
595
+ Paragraph(f'DATE: {all_time_dict["high"]["date"]:%d-%b-%y}', style=s_table_center),
596
+ Paragraph(f'DATE: {all_time_dict["low"]["date"]:%d-%b-%y}', style=s_table_center),
597
+ ],
598
+ )
599
+ table_data.append(
600
+ [
601
+ Paragraph("", style=s_table_center),
602
+ Paragraph("", style=s_table_center),
603
+ ]
604
+ )
605
+
606
+ row_heights = [0.74 * cm, 0.17 * cm, None, None, 0.17 * cm]
607
+
608
+ t = Table(table_data, rowHeights=row_heights)
609
+ t.setStyle(
610
+ TableStyle(
611
+ [
612
+ ("BACKGROUND", (0, 0), (1, 0), c_table_background),
613
+ ("LINEABOVE", (0, 0), (1, 0), 0.25, colors.HexColor("#6d7683")),
614
+ ("LINEBELOW", (0, 0), (1, 0), 0.25, colors.HexColor("#6d7683")),
615
+ ("LINEBELOW", (0, -1), (1, -1), 0.25, colors.HexColor("#6d7683")),
616
+ ("LINEBEFORE", (1, 0), (1, -1), 0.25, colors.HexColor("#6d7683")),
617
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
618
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
619
+ ("RIGHTPADDING", (0, 0), (-1, -1), 0),
620
+ ]
621
+ )
622
+ )
623
+
624
+ elements.append(t)
625
+
626
+ elements.append(platypus.PageBreak("second_page"))
627
+ elements.append(generate_title(general_data["title"]))
628
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
629
+
630
+ def get_bar_chart_height(df, bar_width=0.12 * cm, bar_padding=0.3 * cm, max_label_width=3.35 * cm):
631
+ number_of_items = len(df)
632
+ return number_of_items * (bar_width + 2 * bar_padding)
633
+
634
+ def get_bar_chart(df, width, height, bar_width=0.12 * cm, bar_padding=0.3 * cm, max_label_width=3.35 * cm):
635
+ font_size = 6
636
+ drawing = Drawing(width, height)
637
+ bar_chart = HorizontalBarChart()
638
+
639
+ data = list()
640
+ categories = list()
641
+ for index, row in enumerate(df.itertuples()):
642
+ data.append(row.weighting * 100)
643
+ categories.append(f"{row.aggregated_title} ({row.weighting * 100:.1f}%)")
644
+
645
+ max_label = 0
646
+ _categories = list()
647
+ for category in categories:
648
+ _w = stringWidth(category, "customfont", font_size)
649
+ if _w > max_label_width:
650
+ split_list = category.split(" ")
651
+ splitter = int(len(split_list) * 2 / 3)
652
+
653
+ part_1 = " ".join(split_list[:splitter])
654
+ part_2 = " ".join(split_list[splitter:])
655
+ category = part_1 + "\n" + part_2
656
+ _w1 = stringWidth(part_1, "customfont", font_size)
657
+ _w2 = stringWidth(part_2, "customfont", font_size)
658
+ _w = max(_w1, _w2)
659
+ _categories.append(category)
660
+ max_label = max(max_label, _w - bar_chart.categoryAxis.labels.dx)
661
+
662
+ bar_chart.width = width - max_label
663
+
664
+ bar_chart.height = height
665
+ bar_chart.x = width - bar_chart.width
666
+ bar_chart.y = 0
667
+
668
+ bar_chart.data = [list(reversed(data))]
669
+ bar_chart.categoryAxis.categoryNames = list(reversed(_categories))
670
+ bar_chart.categoryAxis.labels.boxAnchor = "e"
671
+ bar_chart.categoryAxis.labels.textAnchor = "end"
672
+ bar_chart.categoryAxis.labels.fontName = "customfont"
673
+ bar_chart.categoryAxis.labels.fontSize = font_size
674
+ bar_chart.categoryAxis.labels.leading = font_size
675
+
676
+ bar_chart.barWidth = bar_width
677
+ bar_chart.bars.strokeColor = colors.transparent
678
+ bar_chart.bars[0].fillColor = c_product
679
+
680
+ # x-Axis
681
+ bar_chart.valueAxis.labelTextFormat = DecimalFormatter(0, suffix="%")
682
+ bar_chart.valueAxis.labels.fontName = "customfont"
683
+ bar_chart.valueAxis.labels.fontSize = 6
684
+
685
+ bar_chart.valueAxis.strokeWidth = -1
686
+ bar_chart.valueAxis.gridStrokeColor = c_grid_color
687
+ bar_chart.valueAxis.gridStrokeDashArray = (0.2, 0, 0.2)
688
+
689
+ bar_chart.valueAxis.visibleGrid = True
690
+ bar_chart.valueAxis.forceZero = True
691
+
692
+ bar_chart.categoryAxis.strokeWidth = 0.5
693
+ bar_chart.categoryAxis.strokeColor = HexColor(0x6D7683)
694
+ bar_chart.categoryAxis.tickLeft = 0
695
+ bar_chart.categoryAxis.labels.fontName = "customfont"
696
+ bar_chart.categoryAxis.labels.fontSize = 6
697
+ drawing.add(bar_chart)
698
+ drawing.add(String(0, -25, "", fontName="customfont", fontSize=6, fillColor=colors.black))
699
+ return drawing
700
+
701
+ NUM_CHARTS_FIRST_ROW = 2
702
+ WIDTH_CHARTS_FIRST_ROW = (
703
+ (CONTENT_WIDTH_PAGE2 - CONTENT_OFFSET) - ((NUM_CHARTS_FIRST_ROW - 1) * TABLE_MARGIN_PAGE2)
704
+ ) / NUM_CHARTS_FIRST_ROW
705
+
706
+ max_height = max(
707
+ [
708
+ get_pie_chart_horizontal_height(geographical, legend_max_cols=10),
709
+ get_pie_chart_horizontal_height(currencies, legend_max_cols=10),
710
+ ]
711
+ )
712
+ geographical_pie_chart = get_pie_chart_horizontal(
713
+ geographical,
714
+ WIDTH_CHARTS_FIRST_ROW,
715
+ max_height,
716
+ general_data["colors"],
717
+ 4.23 * cm,
718
+ legend_max_cols=10,
719
+ legend_x=3.8 * cm,
720
+ )
721
+ currencies_pie_chart = get_pie_chart_horizontal(
722
+ currencies,
723
+ WIDTH_CHARTS_FIRST_ROW,
724
+ max_height,
725
+ general_data["colors"],
726
+ 4.23 * cm,
727
+ legend_max_cols=10,
728
+ legend_x=3.8 * cm,
729
+ )
730
+ max_height2 = max(
731
+ [
732
+ get_pie_chart_horizontal_height(liquidity, legend_max_cols=10),
733
+ get_pie_chart_horizontal_height(allocation, legend_max_cols=10),
734
+ ]
735
+ )
736
+ liquidity_pie_chart = get_pie_chart_horizontal(
737
+ liquidity,
738
+ WIDTH_CHARTS_FIRST_ROW,
739
+ max_height2,
740
+ general_data["colors"],
741
+ 4.23 * cm,
742
+ legend_max_cols=10,
743
+ legend_x=3.8 * cm,
744
+ )
745
+ allocation_pie_chart = get_pie_chart_horizontal(
746
+ allocation,
747
+ WIDTH_CHARTS_FIRST_ROW,
748
+ max_height2,
749
+ general_data["colors"],
750
+ 4.23 * cm,
751
+ legend_max_cols=10,
752
+ legend_x=3.8 * cm,
753
+ )
754
+
755
+ max_height3 = max([get_pie_chart_horizontal_height(marketcap, legend_max_cols=10)])
756
+ marketcap_pie_chart = get_pie_chart_horizontal(
757
+ marketcap,
758
+ WIDTH_CHARTS_FIRST_ROW,
759
+ max_height3,
760
+ general_data["colors"],
761
+ 4.23 * cm,
762
+ legend_max_cols=10,
763
+ legend_x=3.8 * cm,
764
+ )
765
+
766
+ third_td = [
767
+ [
768
+ Spacer(width=0, height=0),
769
+ Paragraph("<strong>Geographical<br />Breakdown</strong>", style=s_table_medium_leading),
770
+ Spacer(width=0, height=0),
771
+ Paragraph("<strong>Currency<br />Exposure</strong>", style=s_table_medium_leading),
772
+ ],
773
+ [
774
+ Spacer(width=0, height=0),
775
+ geographical_pie_chart,
776
+ Spacer(width=0, height=0),
777
+ currencies_pie_chart,
778
+ ],
779
+ [
780
+ Spacer(width=0, height=0),
781
+ Paragraph("<strong>Liquidity</strong>", style=s_table_medium_leading),
782
+ Spacer(width=0, height=0),
783
+ Paragraph("<strong>Asset<br />Allocation</strong>", style=s_table_medium_leading),
784
+ ],
785
+ [
786
+ Spacer(width=0, height=0),
787
+ liquidity_pie_chart,
788
+ Spacer(width=0, height=0),
789
+ allocation_pie_chart,
790
+ ],
791
+ [
792
+ Spacer(width=0, height=0),
793
+ Paragraph(
794
+ f"<strong>Market Cap.<br />Distributions ({context['currency']})</strong>",
795
+ style=s_table_medium_leading,
796
+ ),
797
+ Spacer(width=0, height=0),
798
+ Spacer(width=0, height=0),
799
+ ],
800
+ [
801
+ Spacer(width=0, height=0),
802
+ marketcap_pie_chart,
803
+ Spacer(width=0, height=0),
804
+ Spacer(width=0, height=0),
805
+ ],
806
+ ]
807
+
808
+ cols = [CONTENT_OFFSET]
809
+ cols.extend([WIDTH_CHARTS_FIRST_ROW, TABLE_MARGIN_PAGE2] * NUM_CHARTS_FIRST_ROW)
810
+ cols.pop(-1)
811
+
812
+ third_table_styles = [
813
+ ("LINEABOVE", (1, 0), (1, 0), 0.25, c_box_color),
814
+ ("LINEBELOW", (1, 0), (1, 0), 0.25, c_box_color),
815
+ ("LINEABOVE", (3, 0), (3, 0), 0.25, c_box_color),
816
+ ("LINEBELOW", (3, 0), (3, 0), 0.25, c_box_color),
817
+ ("LINEABOVE", (1, 2), (1, 2), 0.25, c_box_color),
818
+ ("LINEBELOW", (1, 2), (1, 2), 0.25, c_box_color),
819
+ ("LINEABOVE", (3, 2), (3, 2), 0.25, c_box_color),
820
+ ("LINEBELOW", (3, 2), (3, 2), 0.25, c_box_color),
821
+ ("LINEABOVE", (1, 4), (1, 4), 0.25, c_box_color),
822
+ ("LINEBELOW", (1, 4), (1, 4), 0.25, c_box_color),
823
+ ("VALIGN", (0, 0), (-1, 0), "MIDDLE"),
824
+ ("VALIGN", (0, 2), (-1, 2), "MIDDLE"),
825
+ ("VALIGN", (0, 4), (-1, 4), "MIDDLE"),
826
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
827
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
828
+ ]
829
+
830
+ third_t = Table(
831
+ third_td,
832
+ colWidths=cols,
833
+ rowHeights=[
834
+ 1.4 * cm,
835
+ max_height,
836
+ 1.4 * cm,
837
+ max_height2,
838
+ 1.4 * cm,
839
+ max_height3,
840
+ ],
841
+ )
842
+
843
+ third_t.setStyle(TableStyle(third_table_styles))
844
+ # elements.append(platypus.PageBreak("second_page"))
845
+ # elements.append(generate_title(general_data["title"]))
846
+ # elements.append(Spacer(height=LINEHEIGHT, width=0))
847
+ elements.append(third_t)
848
+
849
+ theme_breakdown = ThemeBreakdown(theme_breakdown_df, 8.94 * cm, c_grid_color)
850
+ risk = RiskScale(
851
+ round(general_data["risk_scale"]),
852
+ para_style=s_base_small_justified,
853
+ text=f'This risk ({general_data["risk_scale"]:.1f}) was calculated manually by weighing the risk of all implemented strategies.',
854
+ )
855
+
856
+ max_available_height = CONTENT_HEIGHT - 1.4 * cm - max_height - 0.85 * cm - 0.41 * cm - LINEHEIGHT
857
+ industry_height = get_bar_chart_height(industry)
858
+ left_height = 2 * 0.85 * cm + theme_breakdown.height + risk.height
859
+ right_height = max(min(max_available_height, industry_height), left_height)
860
+
861
+ right_height_diff = right_height - left_height
862
+
863
+ industry_chart = get_bar_chart(industry, 8.94 * cm, right_height - 30)
864
+
865
+ second_charts = [
866
+ [
867
+ Spacer(width=0, height=0),
868
+ Paragraph("<strong>Theme Breakdown and Contribution</strong>", style=s_table_medium_leading),
869
+ Spacer(width=0, height=0),
870
+ Paragraph("<strong>Industry Exposure</strong>", style=s_table_medium_leading),
871
+ ],
872
+ [
873
+ Spacer(width=0, height=0),
874
+ theme_breakdown,
875
+ # Spacer(width=0, height=0),
876
+ # ta_pie_chart,
877
+ Spacer(width=0, height=0),
878
+ # Spacer(width=0, height=0),
879
+ # Spacer(width=0, height=0),
880
+ industry_chart,
881
+ ],
882
+ # [
883
+ # Spacer(width=0, height=0),
884
+ # contrib_bar_chart,
885
+ # Spacer(width=0, height=0),
886
+ # Spacer(width=0, height=0),
887
+ # ],
888
+ [
889
+ Spacer(width=0, height=0),
890
+ Paragraph("<strong>Risk Scale</strong>", style=s_table_medium_leading),
891
+ Spacer(width=0, height=0),
892
+ Spacer(width=0, height=0),
893
+ ],
894
+ [
895
+ Spacer(width=0, height=0),
896
+ risk,
897
+ # RiskScaleFlowable(risk_height, round(general_data["risk_scale"]), f"The risk scale ({general_data["risk_scale"]:.1f}) is computed manually by weighing the risk of each implemented strategy."),
898
+ # contrib_bar_chart,
899
+ Spacer(width=0, height=0),
900
+ Spacer(width=0, height=0),
901
+ ],
902
+ ]
903
+
904
+ col_width = (CONTENT_WIDTH_PAGE2 - CONTENT_OFFSET - TABLE_MARGIN_PAGE2) / 2
905
+
906
+ other_table = Table(
907
+ second_charts,
908
+ colWidths=[CONTENT_OFFSET, col_width, TABLE_MARGIN_PAGE2, col_width],
909
+ rowHeights=[
910
+ 0.85 * cm,
911
+ theme_breakdown.height,
912
+ # ta_height,
913
+ 0.85 * cm,
914
+ risk.height + right_height_diff,
915
+ ],
916
+ )
917
+
918
+ other_table_styles = [
919
+ ("VALIGN", (1, 1), (1, 1), "TOP"),
920
+ ("SPAN", (3, 1), (3, -1)),
921
+ ("VALIGN", (3, 1), (3, -1), "TOP"),
922
+ ("LINEABOVE", (1, 0), (1, 0), 0.25, c_box_color),
923
+ ("LINEBELOW", (1, 0), (1, 0), 0.25, c_box_color),
924
+ ("LINEABOVE", (3, 0), (3, 0), 0.25, c_box_color),
925
+ ("LINEBELOW", (3, 0), (3, 0), 0.25, c_box_color),
926
+ ("LINEABOVE", (1, 2), (1, 2), 0.25, c_box_color),
927
+ ("LINEBELOW", (1, 2), (1, 2), 0.25, c_box_color),
928
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
929
+ ]
930
+ if debug:
931
+ other_table_styles.extend(
932
+ [
933
+ ("BOX", (0, 0), (-1, -1), 0.25, c_box_color),
934
+ ("INNERGRID", (0, 0), (-1, -1), 0.25, c_box_color),
935
+ ]
936
+ )
937
+
938
+ other_table.setStyle(TableStyle(other_table_styles))
939
+ elements.append(platypus.PageBreak("second_page"))
940
+ elements.append(generate_title(general_data["title"]))
941
+ elements.append(Spacer(height=LINEHEIGHT, width=0))
942
+ elements.append(other_table)
943
+
944
+ doc.build(elements)
945
+ output.seek(0)
946
+
947
+ return output