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,872 @@
1
+ from datetime import date
2
+ from decimal import Decimal
3
+ from io import BytesIO
4
+
5
+ from reportlab import platypus
6
+ from reportlab.graphics import renderPDF
7
+ from reportlab.graphics.charts.barcharts import HorizontalBarChart
8
+ from reportlab.graphics.shapes import Drawing, String
9
+ from reportlab.lib import colors
10
+ from reportlab.lib.colors import HexColor
11
+ from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_RIGHT
12
+ from reportlab.lib.formatters import DecimalFormatter
13
+ from reportlab.lib.pagesizes import A4
14
+ from reportlab.lib.styles import ParagraphStyle
15
+ from reportlab.lib.units import cm
16
+ from reportlab.pdfbase import pdfmetrics
17
+ from reportlab.pdfbase.pdfmetrics import registerFontFamily, stringWidth
18
+ from reportlab.pdfbase.ttfonts import TTFont
19
+ from reportlab.platypus import (
20
+ BaseDocTemplate,
21
+ Image,
22
+ NextPageTemplate,
23
+ PageTemplate,
24
+ Paragraph,
25
+ Spacer,
26
+ Table,
27
+ TableStyle,
28
+ )
29
+ from reportlab.platypus.flowables import KeepTogether, TopPadder
30
+ from reportlab.platypus.frames import Frame
31
+ from svglib.svglib import svg2rlg
32
+ from wbreport.models import ReportAsset
33
+ from wbreport.pdf.charts.pie import (
34
+ get_pie_chart_horizontal,
35
+ get_pie_chart_horizontal_height,
36
+ )
37
+ from wbreport.pdf.charts.timeseries import Scale, get_timeseries_chart
38
+ from wbreport.pdf.flowables.risk import RiskScale
39
+ from wbreport.pdf.flowables.textboxes import TextBox, TextBoxWithImage, TextWithIcon
40
+ from wbreport.pdf.flowables.themes import ThemeBreakdown
41
+ from wbreport.pdf.tables.aggregated_tables import get_simple_aggregated_table
42
+ from wbreport.pdf.tables.data_tables import get_simple_data_table
43
+
44
+
45
+ def generate_report(context):
46
+ debug = False
47
+
48
+ main_features_dict = context["information_table"]["Main Features"]
49
+ # Monthly returns table as dataframe, None is no value
50
+ monthly_returns = context["monthly_returns"]
51
+
52
+ # Pie chart Dataframe
53
+ geographical = context["geographical_breakdown"]
54
+ currencies = context["currency_exposure"]
55
+ allocation = context["asset_allocation"]
56
+ marketcap = context["market_cap_distribution"]
57
+ liquidity = context["equity_liquidity"]
58
+ industry = context["industry_exposure"]
59
+
60
+ # Price time serie as dataframe
61
+ prices = context["prices"]
62
+
63
+ # TOPs as list
64
+
65
+ top_3_holdings = context["holdings"][0:3]
66
+
67
+ top_3_contributors = context["top_contributors"]
68
+ bottom_3_contributors = context["worst_contributors"]
69
+
70
+ # {'high': {'price':low, 'date':date}, 'low' : {'price':high, 'date':date}}
71
+ all_time_dict = context["all_time"]
72
+ end = context["date"]
73
+ general_data = {}
74
+ general_data["date"] = end.strftime("%b %Y")
75
+ general_data["colors"] = context["colors_palette"]
76
+ general_data["title"] = context["title"].replace("&", "&")
77
+ general_data["risk_scale"] = context["risk_scale"]
78
+
79
+ general_data["introduction"] = context["introduction"]
80
+ theme_breakdown_df = context["strategy_allocation"]
81
+
82
+ # Register Fonts
83
+ pdfmetrics.registerFont(TTFont("customfont", ReportAsset.objects.get(key="font-default").asset))
84
+ pdfmetrics.registerFont(TTFont("customfont-bd", ReportAsset.objects.get(key="font-bd").asset))
85
+ pdfmetrics.registerFont(TTFont("customfont-it", ReportAsset.objects.get(key="font-it").asset))
86
+ registerFontFamily("customfont", normal="customfont", bold="customfont-bd", italic="customfont-it")
87
+
88
+ # Page Variables
89
+ LINEHEIGHT = 12
90
+
91
+ HEADER_HEIGHT = 2.34 * cm
92
+ FOOTER_HEIGHT = 2.34 * cm
93
+
94
+ SIDE_MARGIN = 0.96 * cm
95
+ TOP_MARGIN = 3.63 * cm
96
+ CONTENT_OFFSET = 0.34 * cm
97
+ CONTENT_MARGIN = SIDE_MARGIN + CONTENT_OFFSET
98
+
99
+ CONTENT_HEIGHT = 22.25 * cm
100
+ CONTENT_WIDTH_PAGE1_LEFT = 13.84 * cm
101
+ CONTENT_WIDTH_PAGE1_RIGHT = 4.23 * cm
102
+ CONTENT_WIDTH_PAGE2 = A4[0] - SIDE_MARGIN - CONTENT_MARGIN
103
+
104
+ CONTENT_X_PAGE1_RIGHT = A4[0] - CONTENT_MARGIN - CONTENT_WIDTH_PAGE1_RIGHT
105
+ CONTENT_Y = A4[1] - CONTENT_HEIGHT - TOP_MARGIN
106
+
107
+ TABLE_MARGIN_PAGE2 = 0.56 * cm
108
+
109
+ output = BytesIO()
110
+ doc = BaseDocTemplate(
111
+ output,
112
+ pagesize=A4,
113
+ rightMargin=0,
114
+ leftMargin=0,
115
+ topMargin=0,
116
+ bottomMargin=0,
117
+ title=general_data["title"],
118
+ author="Atonra Partners SA",
119
+ )
120
+ elements = []
121
+
122
+ s_base = ParagraphStyle(name="s_base", fontName="customfont", fontSize=9, leading=10)
123
+ s_base_small_justified = ParagraphStyle(
124
+ name="s_base_small_justified", parent=s_base, fontSize=6.5, leading=7, alignment=TA_JUSTIFY
125
+ )
126
+ s_base_indent = ParagraphStyle(name="s_description", parent=s_base, spaceBefore=8, leftIndent=CONTENT_OFFSET)
127
+ s_table_base = ParagraphStyle(
128
+ name="s_table_base",
129
+ fontName="customfont",
130
+ fontSize=6,
131
+ leading=6,
132
+ )
133
+ s_table_medium = ParagraphStyle(name="s_table_medium", parent=s_table_base, fontSize=9, leading=8)
134
+ s_table_medium_leading = ParagraphStyle(name="s_table_medium_leading", parent=s_table_medium, leading=13.9)
135
+ s_table_large = ParagraphStyle(name="s_table_large", parent=s_table_medium, fontSize=11, leading=11)
136
+ s_table_large_center = ParagraphStyle(name="s_table_large", parent=s_table_large, alignment=TA_CENTER)
137
+ s_table_center = ParagraphStyle(
138
+ name="s_table_center",
139
+ parent=s_table_base,
140
+ alignment=TA_CENTER,
141
+ )
142
+ s_table_center_high = ParagraphStyle(name="s_table_center", parent=s_table_center, leading=9, fontSize=8)
143
+ s_table_right = ParagraphStyle(name="s_table_right", parent=s_table_base, alignment=TA_RIGHT)
144
+ s_table_center = ParagraphStyle(name="s_table_center", parent=s_table_base, alignment=TA_CENTER)
145
+ s_table_headline = ParagraphStyle(
146
+ name="s_table_headline",
147
+ parent=s_table_base,
148
+ fontSize=16,
149
+ leading=16,
150
+ )
151
+ s_table_headline_2 = ParagraphStyle(
152
+ name="s_table_headline_2",
153
+ parent=s_table_base,
154
+ fontSize=10,
155
+ leading=10,
156
+ )
157
+
158
+ # Setup Colors
159
+ c_product = HexColor(general_data["colors"][0])
160
+ c_product_alpha = HexColor(f"{general_data['colors'][0]}20", hasAlpha=True)
161
+
162
+ c_table_border = HexColor(0x9EA3AC)
163
+ c_grid_color = HexColor(0xB6BAC1)
164
+ c_box_color = HexColor(0x3C4859)
165
+ c_table_background = colors.HexColor(0xE2E3E6)
166
+
167
+ # Frame and Page Layout
168
+ frame_defaults = {"showBoundary": debug, "leftPadding": 0, "rightPadding": 0, "topPadding": 0, "bottomPadding": 0}
169
+
170
+ text_frame = Frame(
171
+ x1=SIDE_MARGIN,
172
+ y1=CONTENT_Y,
173
+ width=CONTENT_WIDTH_PAGE1_LEFT,
174
+ height=CONTENT_HEIGHT,
175
+ id="text_frame",
176
+ **frame_defaults,
177
+ )
178
+
179
+ main_features_frame = Frame(
180
+ x1=CONTENT_X_PAGE1_RIGHT,
181
+ y1=CONTENT_Y,
182
+ width=CONTENT_WIDTH_PAGE1_RIGHT,
183
+ height=CONTENT_HEIGHT,
184
+ id="main_features_frame",
185
+ **frame_defaults,
186
+ )
187
+
188
+ second_page = Frame(
189
+ x1=SIDE_MARGIN,
190
+ y1=CONTENT_Y,
191
+ width=CONTENT_WIDTH_PAGE2,
192
+ height=CONTENT_HEIGHT,
193
+ id="second_page",
194
+ **frame_defaults,
195
+ )
196
+
197
+ def on_page(canv, dock):
198
+ canv.saveState()
199
+
200
+ # Header
201
+ canv.setFillColor(c_box_color)
202
+ canv.rect(0, A4[1] - HEADER_HEIGHT, A4[0], HEADER_HEIGHT, fill=True, stroke=False)
203
+
204
+ colors = [
205
+ HexColor(0xFFB166),
206
+ HexColor(0x8CD66B),
207
+ HexColor(0x05D6A1),
208
+ HexColor(0x01ABAA),
209
+ HexColor(0x70D6FF),
210
+ HexColor(0x0585FF),
211
+ HexColor(0x5724D9),
212
+ HexColor(0xA359E5),
213
+ HexColor(0xEF476F),
214
+ ]
215
+ height = 0.13 * cm
216
+ width = A4[0] / len(colors)
217
+ for index, color in enumerate(colors):
218
+ canv.setFillColor(color)
219
+ canv.rect(0 + index * width, A4[1] - HEADER_HEIGHT - height, width, height, fill=True, stroke=False)
220
+
221
+ # Footer
222
+ canv.setFillColor(c_box_color)
223
+ canv.rect(0, 0, A4[0], FOOTER_HEIGHT, fill=True, stroke=False)
224
+
225
+ for index, color in enumerate(colors):
226
+ canv.setFillColor(color)
227
+ canv.rect(0 + index * width, FOOTER_HEIGHT, width, height, fill=True, stroke=False)
228
+
229
+ drawing = svg2rlg(ReportAsset.objects.get(key="logo").asset)
230
+ width, height = drawing.width, drawing.height
231
+ height = 1.295 * cm
232
+
233
+ scaling_factor = 1.295 * cm / drawing.height
234
+ drawing.scale(scaling_factor, scaling_factor)
235
+ renderPDF.draw(
236
+ drawing, canv, ((A4[0] - width * scaling_factor) / 2), A4[1] - height - (HEADER_HEIGHT - height) / 2
237
+ )
238
+
239
+ reportlab_image = Image(ReportAsset.objects.get(key="footer-text").asset)
240
+ width, height = reportlab_image.imageWidth, reportlab_image.imageHeight
241
+ ratio = float(width) / float(height)
242
+ height = 0.436 * cm
243
+ width = height * ratio
244
+
245
+ reportlab_image.drawWidth = width
246
+ reportlab_image.drawHeight = height
247
+ reportlab_image.drawOn(canv, (A4[0] - width) / 2, (FOOTER_HEIGHT - height) / 2)
248
+
249
+ canv.restoreState()
250
+
251
+ doc.addPageTemplates(
252
+ [
253
+ PageTemplate(id="page", onPage=on_page, frames=[text_frame, main_features_frame]),
254
+ PageTemplate(id="second_page", onPage=on_page, frames=[second_page]),
255
+ ]
256
+ )
257
+
258
+ elements.append(NextPageTemplate(["second_page"]))
259
+
260
+ def generate_title(title):
261
+ table_data = [[Paragraph("", style=s_table_headline), Paragraph(title, style=s_table_headline)]]
262
+ title_table = Table(table_data, colWidths=[0.14 * cm, None], rowHeights=[0.41 * cm])
263
+ title_table.setStyle(
264
+ TableStyle(
265
+ [
266
+ ("BACKGROUND", (0, 0), (0, 0), c_product),
267
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
268
+ ("LEFTPADDING", (1, 0), (-1, -1), 0.2 * cm),
269
+ ("RIGHTPADDING", (0, 0), (-1, -1), 0),
270
+ ("TOPPADDING", (0, 0), (-1, -1), 0),
271
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
272
+ ]
273
+ )
274
+ )
275
+ return title_table
276
+
277
+ def impress(_list):
278
+ style = s_table_headline_2
279
+ table_data = [
280
+ [
281
+ Paragraph("", style=style),
282
+ Paragraph("Important information", style=style),
283
+ ],
284
+ [
285
+ Paragraph("", style=style),
286
+ Paragraph(ReportAsset.objects.get(key="disclaimer").text, style=s_base_small_justified),
287
+ ],
288
+ ]
289
+
290
+ t = Table(table_data, colWidths=[0.14 * cm, None], rowHeights=[0.41 * cm, None])
291
+ t.setStyle(
292
+ TableStyle(
293
+ [
294
+ ("BACKGROUND", (0, 0), (0, 0), colors.white),
295
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
296
+ ("LEFTPADDING", (1, 0), (-1, -1), 0.2 * cm),
297
+ ("RIGHTPADDING", (0, 0), (-1, -1), 0),
298
+ ("TOPPADDING", (0, 0), (-1, -1), 0),
299
+ ("TOPPADDING", (0, 1), (-1, -1), 0.334 * cm),
300
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
301
+ # ("BOTTOMPADDING", (0, -1), (-1, -1), 0.7*cm),
302
+ ]
303
+ )
304
+ )
305
+ _list.append(KeepTogether(TopPadder(t)))
306
+
307
+ # Description
308
+ elements.append(generate_title(general_data["title"]))
309
+ elements.append(Paragraph(general_data["introduction"], style=s_base_indent))
310
+ elements.append(Spacer(height=LINEHEIGHT * 2, width=0))
311
+
312
+ # Monthly Returns
313
+ elements.append(generate_title("Monthly Returns (%)"))
314
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
315
+ elements.append(
316
+ get_simple_aggregated_table(
317
+ df=monthly_returns,
318
+ width=CONTENT_WIDTH_PAGE1_LEFT,
319
+ row_height=0.389 * cm,
320
+ header_style=s_table_center,
321
+ row_style=s_table_center,
322
+ data_style=s_table_right,
323
+ grid_color=c_table_border,
324
+ offset=CONTENT_OFFSET,
325
+ debug=debug,
326
+ )
327
+ )
328
+ elements.append(Spacer(height=LINEHEIGHT * 2, width=0))
329
+
330
+ # Price Timeseries Chart
331
+ elements.append(
332
+ get_timeseries_chart(
333
+ data=[list(zip(prices.index, prices.net_value))],
334
+ width=CONTENT_WIDTH_PAGE1_LEFT - CONTENT_OFFSET,
335
+ height=4.34 * cm,
336
+ color=c_product,
337
+ fill_color=c_product_alpha,
338
+ grid_color=c_grid_color,
339
+ scale=Scale.LOGARITHMIC.value,
340
+ debug=debug,
341
+ x=CONTENT_OFFSET,
342
+ )
343
+ )
344
+ elements.append(Spacer(height=LINEHEIGHT * 2, width=0))
345
+
346
+ # Top 3
347
+ elements.append(
348
+ get_simple_data_table(
349
+ headers=[
350
+ "<strong>Top 3 Holdings</strong>",
351
+ "<strong>Top 3 Contributors</strong>",
352
+ "<strong>Bottom 3 Contributors</strong>",
353
+ ],
354
+ data=list(zip(top_3_holdings, top_3_contributors, bottom_3_contributors)),
355
+ width=CONTENT_WIDTH_PAGE1_LEFT,
356
+ header_row_height=0.85 * cm,
357
+ data_row_height=0.39 * cm,
358
+ margin=0.16 * cm,
359
+ header_style=s_table_medium,
360
+ data_style=s_table_base,
361
+ grid_color=c_table_border,
362
+ offset=CONTENT_OFFSET,
363
+ debug=debug,
364
+ )
365
+ )
366
+
367
+ # More Information Box
368
+ elements.append(
369
+ TopPadder(
370
+ TextBoxWithImage(
371
+ width=CONTENT_WIDTH_PAGE1_LEFT,
372
+ height=1.36 * cm,
373
+ img=ReportAsset.objects.get(key="robot").asset,
374
+ img_x=0.516 * cm,
375
+ img_y=-0.8 * cm,
376
+ img_width=2.89 * cm,
377
+ img_height=2.89 * cm,
378
+ text='<strong>Click here to learn more:</strong> <a href="https://www.atonra.ch/our-research/">www.atonra.ch/our-research/</a>',
379
+ text_style=s_table_center_high,
380
+ box_color=c_table_background,
381
+ offset=CONTENT_OFFSET,
382
+ debug=debug,
383
+ )
384
+ )
385
+ )
386
+
387
+ elements.append(platypus.FrameBreak("main_features_frame"))
388
+
389
+ # Main Features Table
390
+
391
+ MAIN_FEATURES_COLOR_BAR_HEIGHT = 0.23 * cm
392
+ MAIN_FEATURES_GAP_HEIGHT = 0.17 * cm
393
+ MAIN_FEATURES_TITLE_HEIGHT = 1.94 * cm
394
+ MAIN_FEATURES_TITLE1_HEIGHT = 0.74 * cm
395
+
396
+ table_data = [
397
+ [Spacer(width=0, height=0)],
398
+ [
399
+ TextWithIcon(
400
+ width=CONTENT_WIDTH_PAGE1_RIGHT,
401
+ height=MAIN_FEATURES_TITLE_HEIGHT,
402
+ text=general_data["date"],
403
+ font="customfont-bd",
404
+ font_size=11,
405
+ icon=context["logo_file"] if "logo_file" in context else None,
406
+ )
407
+ ],
408
+ [Paragraph("<strong>MAIN FEATURES</strong>", style=s_table_center)],
409
+ [Spacer(width=0, height=0)],
410
+ ]
411
+
412
+ for label, value in main_features_dict.items():
413
+ if isinstance(value, date):
414
+ value = value.strftime("%d-%b-%y")
415
+ elif isinstance(value, (Decimal, float)):
416
+ value = "%.2f" % value
417
+ elif isinstance(value, int):
418
+ value = str(value)
419
+ elif value is None:
420
+ value = ""
421
+
422
+ if label.lower() in ["currency", "last price"]:
423
+ table_data.append(
424
+ [
425
+ Paragraph(f"<strong>{label.upper()}</strong>", style=s_table_base),
426
+ Paragraph(f"<strong>{value.upper()}</strong>", style=s_table_base),
427
+ ]
428
+ )
429
+ else:
430
+ table_data.append(
431
+ [
432
+ Paragraph(label.upper(), style=s_table_base),
433
+ Paragraph(value.upper(), style=s_table_base),
434
+ ]
435
+ )
436
+
437
+ table_data.extend([[Spacer(width=0, height=0)], [Spacer(width=0, height=0)]])
438
+
439
+ row_heights = [
440
+ MAIN_FEATURES_COLOR_BAR_HEIGHT,
441
+ MAIN_FEATURES_TITLE_HEIGHT,
442
+ MAIN_FEATURES_TITLE1_HEIGHT,
443
+ MAIN_FEATURES_GAP_HEIGHT,
444
+ ]
445
+ row_heights.extend([None] * (len(table_data) - 6))
446
+ row_heights.extend([MAIN_FEATURES_GAP_HEIGHT, MAIN_FEATURES_COLOR_BAR_HEIGHT])
447
+
448
+ t = Table(table_data, rowHeights=row_heights, colWidths=[CONTENT_WIDTH_PAGE1_RIGHT / 2] * 2)
449
+ table_styles = [
450
+ ("BACKGROUND", (0, 0), (1, 0), c_product), # Top Color Bar
451
+ ("BACKGROUND", (0, -1), (1, -1), c_product), # Bottom Color Bar
452
+ ("BACKGROUND", (0, 2), (-1, 2), c_table_background), # Title Background
453
+ ("LINEABOVE", (0, 2), (-1, 2), 0.25, colors.HexColor("#6d7683")), # Title Line Above
454
+ ("LINEBELOW", (0, 2), (-1, 2), 0.25, colors.HexColor("#6d7683")), # Title Line Below
455
+ ("LINEBEFORE", (1, 3), (1, -2), 0.25, colors.HexColor("#6d7683")), # Data Vertical Seperator
456
+ ("SPAN", (0, 1), (-1, 1)), # Col Span Title
457
+ ("SPAN", (0, 2), (-1, 2)), # Col Span Title1
458
+ ("LEFTPADDING", (0, 1), (0, -1), 0), # Leftpadding Data Labels
459
+ ("LEFTPADDING", (1, 1), (1, -1), 0.28 * cm), # Leftpadding Data Values
460
+ ("VALIGN", (0, 1), (-1, -1), "MIDDLE"),
461
+ ]
462
+
463
+ if debug:
464
+ table_styles.extend(
465
+ [
466
+ ("INNERGRID", (0, 0), (-1, -1), 0.25, colors.black),
467
+ ("BOX", (0, 0), (-1, -1), 0.25, colors.black),
468
+ ]
469
+ )
470
+
471
+ t.setStyle(TableStyle(table_styles))
472
+
473
+ elements.append(t)
474
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
475
+
476
+ # All time table
477
+ table_data = [
478
+ [
479
+ Paragraph("<strong>ALL TIME HIGH</strong>", style=s_table_center),
480
+ Paragraph("<strong>ALL TIME LOW</strong>", style=s_table_center),
481
+ ],
482
+ [
483
+ Paragraph("", style=s_table_center),
484
+ Paragraph("", style=s_table_center),
485
+ ],
486
+ ]
487
+
488
+ table_data.append(
489
+ [
490
+ Paragraph(f'PRICE: {all_time_dict["high"]["price"]:.1f}', style=s_table_center),
491
+ Paragraph(f'PRICE: {all_time_dict["low"]["price"]:.1f}', style=s_table_center),
492
+ ],
493
+ )
494
+ table_data.append(
495
+ [
496
+ Paragraph(f'DATE: {all_time_dict["high"]["date"]:%d-%b-%y}', style=s_table_center),
497
+ Paragraph(f'DATE: {all_time_dict["low"]["date"]:%d-%b-%y}', style=s_table_center),
498
+ ],
499
+ )
500
+ table_data.append(
501
+ [
502
+ Paragraph("", style=s_table_center),
503
+ Paragraph("", style=s_table_center),
504
+ ]
505
+ )
506
+
507
+ row_heights = [0.74 * cm, 0.17 * cm, None, None, 0.17 * cm]
508
+
509
+ t = Table(table_data, rowHeights=row_heights)
510
+ t.setStyle(
511
+ TableStyle(
512
+ [
513
+ ("BACKGROUND", (0, 0), (1, 0), c_table_background),
514
+ ("LINEABOVE", (0, 0), (1, 0), 0.25, colors.HexColor("#6d7683")),
515
+ ("LINEBELOW", (0, 0), (1, 0), 0.25, colors.HexColor("#6d7683")),
516
+ ("LINEBELOW", (0, -1), (1, -1), 0.25, colors.HexColor("#6d7683")),
517
+ ("LINEBEFORE", (1, 0), (1, -1), 0.25, colors.HexColor("#6d7683")),
518
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
519
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
520
+ ("RIGHTPADDING", (0, 0), (-1, -1), 0),
521
+ ]
522
+ )
523
+ )
524
+
525
+ elements.append(t)
526
+
527
+ elements.append(
528
+ TopPadder(
529
+ TextBox(
530
+ width=CONTENT_WIDTH_PAGE1_RIGHT,
531
+ height=4.37 * cm,
532
+ text='<strong>Investment Team:</strong><br />a seasoned team of<br />portfolio managers /<br />analysts and engineers<br />supported by the full<br />resources of AtonRâ<br />Partners.<br /><br /><strong><a href="https://atonra.ch/who-we-are/our-team/">Click here to discover<br />the entire team</a></strong>',
533
+ text_style=s_table_center_high,
534
+ box_color=c_table_background,
535
+ debug=debug,
536
+ )
537
+ )
538
+ )
539
+
540
+ # Second Page
541
+ elements.append(platypus.PageBreak("second_page"))
542
+ elements.append(generate_title(general_data["title"]))
543
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
544
+
545
+ def get_bar_chart_height(df, bar_width=0.12 * cm, bar_padding=0.3 * cm, max_label_width=3.35 * cm):
546
+ number_of_items = len(df)
547
+ return number_of_items * (bar_width + 2 * bar_padding)
548
+
549
+ def get_bar_chart(df, width, height, bar_width=0.12 * cm, bar_padding=0.3 * cm, max_label_width=3.35 * cm):
550
+ font_size = 6
551
+ drawing = Drawing(width, height)
552
+ bar_chart = HorizontalBarChart()
553
+
554
+ data = list()
555
+ categories = list()
556
+ for index, row in enumerate(df.itertuples()):
557
+ data.append(row.weighting * 100)
558
+ categories.append(f"{row.aggregated_title} ({row.weighting*100:.1f}%)")
559
+
560
+ max_label = 0
561
+ _categories = list()
562
+ for category in categories:
563
+ _w = stringWidth(category, "customfont", font_size)
564
+ if _w > max_label_width:
565
+ split_list = category.split(" ")
566
+ splitter = int(len(split_list) * 2 / 3)
567
+
568
+ part_1 = " ".join(split_list[:splitter])
569
+ part_2 = " ".join(split_list[splitter:])
570
+ category = part_1 + "\n" + part_2
571
+ _w1 = stringWidth(part_1, "customfont", font_size)
572
+ _w2 = stringWidth(part_2, "customfont", font_size)
573
+ _w = max(_w1, _w2)
574
+ _categories.append(category)
575
+ max_label = max(max_label, _w - bar_chart.categoryAxis.labels.dx)
576
+
577
+ bar_chart.width = width - max_label
578
+
579
+ bar_chart.height = height
580
+ bar_chart.x = width - bar_chart.width
581
+ bar_chart.y = 0
582
+
583
+ bar_chart.data = [list(reversed(data))]
584
+ bar_chart.categoryAxis.categoryNames = list(reversed(_categories))
585
+ bar_chart.categoryAxis.labels.boxAnchor = "e"
586
+ bar_chart.categoryAxis.labels.textAnchor = "end"
587
+ bar_chart.categoryAxis.labels.fontName = "customfont"
588
+ bar_chart.categoryAxis.labels.fontSize = font_size
589
+ bar_chart.categoryAxis.labels.leading = font_size
590
+
591
+ bar_chart.barWidth = bar_width
592
+ bar_chart.bars.strokeColor = colors.transparent
593
+ bar_chart.bars[0].fillColor = c_product
594
+
595
+ # x-Axis
596
+ bar_chart.valueAxis.labelTextFormat = DecimalFormatter(0, suffix="%")
597
+ bar_chart.valueAxis.labels.fontName = "customfont"
598
+ bar_chart.valueAxis.labels.fontSize = 6
599
+
600
+ bar_chart.valueAxis.strokeWidth = -1
601
+ bar_chart.valueAxis.gridStrokeColor = c_grid_color
602
+ bar_chart.valueAxis.gridStrokeDashArray = (0.2, 0, 0.2)
603
+
604
+ bar_chart.valueAxis.visibleGrid = True
605
+ bar_chart.valueAxis.forceZero = True
606
+
607
+ bar_chart.categoryAxis.strokeWidth = 0.5
608
+ bar_chart.categoryAxis.strokeColor = HexColor(0x6D7683)
609
+ bar_chart.categoryAxis.tickLeft = 0
610
+ bar_chart.categoryAxis.labels.fontName = "customfont"
611
+ bar_chart.categoryAxis.labels.fontSize = 6
612
+ drawing.add(bar_chart)
613
+ drawing.add(String(0, -25, "", fontName="customfont", fontSize=6, fillColor=colors.black))
614
+ return drawing
615
+
616
+ NUM_CHARTS_FIRST_ROW = 2
617
+ WIDTH_CHARTS_FIRST_ROW = (
618
+ (CONTENT_WIDTH_PAGE2 - CONTENT_OFFSET) - ((NUM_CHARTS_FIRST_ROW - 1) * TABLE_MARGIN_PAGE2)
619
+ ) / NUM_CHARTS_FIRST_ROW
620
+
621
+ max_height = max(
622
+ [
623
+ get_pie_chart_horizontal_height(geographical, legend_max_cols=10),
624
+ get_pie_chart_horizontal_height(currencies, legend_max_cols=10),
625
+ ]
626
+ )
627
+ geographical_pie_chart = get_pie_chart_horizontal(
628
+ geographical,
629
+ WIDTH_CHARTS_FIRST_ROW,
630
+ max_height,
631
+ general_data["colors"],
632
+ 4.23 * cm,
633
+ legend_max_cols=10,
634
+ legend_x=3.8 * cm,
635
+ )
636
+ currencies_pie_chart = get_pie_chart_horizontal(
637
+ currencies,
638
+ WIDTH_CHARTS_FIRST_ROW,
639
+ max_height,
640
+ general_data["colors"],
641
+ 4.23 * cm,
642
+ legend_max_cols=10,
643
+ legend_x=3.8 * cm,
644
+ )
645
+ max_height2 = max(
646
+ [
647
+ get_pie_chart_horizontal_height(liquidity, legend_max_cols=10),
648
+ get_pie_chart_horizontal_height(allocation, legend_max_cols=10),
649
+ ]
650
+ )
651
+ liquidity_pie_chart = get_pie_chart_horizontal(
652
+ liquidity,
653
+ WIDTH_CHARTS_FIRST_ROW,
654
+ max_height2,
655
+ general_data["colors"],
656
+ 4.23 * cm,
657
+ legend_max_cols=10,
658
+ legend_x=3.8 * cm,
659
+ )
660
+ allocation_pie_chart = get_pie_chart_horizontal(
661
+ allocation,
662
+ WIDTH_CHARTS_FIRST_ROW,
663
+ max_height2,
664
+ general_data["colors"],
665
+ 4.23 * cm,
666
+ legend_max_cols=10,
667
+ legend_x=3.8 * cm,
668
+ )
669
+
670
+ max_height3 = max([get_pie_chart_horizontal_height(marketcap, legend_max_cols=10)])
671
+ marketcap_pie_chart = get_pie_chart_horizontal(
672
+ marketcap,
673
+ WIDTH_CHARTS_FIRST_ROW,
674
+ max_height3,
675
+ general_data["colors"],
676
+ 4.23 * cm,
677
+ legend_max_cols=10,
678
+ legend_x=3.8 * cm,
679
+ )
680
+
681
+ third_td = [
682
+ [
683
+ Spacer(width=0, height=0),
684
+ Paragraph("<strong>Geographical<br />Breakdown</strong>", style=s_table_medium_leading),
685
+ Spacer(width=0, height=0),
686
+ Paragraph("<strong>Currency<br />Exposure</strong>", style=s_table_medium_leading),
687
+ ],
688
+ [
689
+ Spacer(width=0, height=0),
690
+ geographical_pie_chart,
691
+ Spacer(width=0, height=0),
692
+ currencies_pie_chart,
693
+ ],
694
+ [
695
+ Spacer(width=0, height=0),
696
+ Paragraph("<strong>Liquidity</strong>", style=s_table_medium_leading),
697
+ Spacer(width=0, height=0),
698
+ Paragraph("<strong>Asset<br />Allocation</strong>", style=s_table_medium_leading),
699
+ ],
700
+ [
701
+ Spacer(width=0, height=0),
702
+ liquidity_pie_chart,
703
+ Spacer(width=0, height=0),
704
+ allocation_pie_chart,
705
+ ],
706
+ [
707
+ Spacer(width=0, height=0),
708
+ Paragraph(
709
+ f"<strong>Market Cap.<br />Distributions ({context['currency']})</strong>",
710
+ style=s_table_medium_leading,
711
+ ),
712
+ Spacer(width=0, height=0),
713
+ Spacer(width=0, height=0),
714
+ ],
715
+ [
716
+ Spacer(width=0, height=0),
717
+ marketcap_pie_chart,
718
+ Spacer(width=0, height=0),
719
+ Spacer(width=0, height=0),
720
+ ],
721
+ ]
722
+
723
+ cols = [CONTENT_OFFSET]
724
+ cols.extend([WIDTH_CHARTS_FIRST_ROW, TABLE_MARGIN_PAGE2] * NUM_CHARTS_FIRST_ROW)
725
+ cols.pop(-1)
726
+
727
+ third_table_styles = [
728
+ ("LINEABOVE", (1, 0), (1, 0), 0.25, c_box_color),
729
+ ("LINEBELOW", (1, 0), (1, 0), 0.25, c_box_color),
730
+ ("LINEABOVE", (3, 0), (3, 0), 0.25, c_box_color),
731
+ ("LINEBELOW", (3, 0), (3, 0), 0.25, c_box_color),
732
+ ("LINEABOVE", (1, 2), (1, 2), 0.25, c_box_color),
733
+ ("LINEBELOW", (1, 2), (1, 2), 0.25, c_box_color),
734
+ ("LINEABOVE", (3, 2), (3, 2), 0.25, c_box_color),
735
+ ("LINEBELOW", (3, 2), (3, 2), 0.25, c_box_color),
736
+ ("LINEABOVE", (1, 4), (1, 4), 0.25, c_box_color),
737
+ ("LINEBELOW", (1, 4), (1, 4), 0.25, c_box_color),
738
+ ("VALIGN", (0, 0), (-1, 0), "MIDDLE"),
739
+ ("VALIGN", (0, 2), (-1, 2), "MIDDLE"),
740
+ ("VALIGN", (0, 4), (-1, 4), "MIDDLE"),
741
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
742
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
743
+ ]
744
+
745
+ third_t = Table(
746
+ third_td,
747
+ colWidths=cols,
748
+ rowHeights=[
749
+ 1.4 * cm,
750
+ max_height,
751
+ 1.4 * cm,
752
+ max_height2,
753
+ 1.4 * cm,
754
+ max_height3,
755
+ ],
756
+ )
757
+
758
+ # if debug:
759
+ # top_3_table_styles.extend(
760
+ # [
761
+ # ("BOX", (0, 0), (-1, -1), 0.25, c_box_color),
762
+ # ("INNERGRID", (0, 0), (-1, -1), 0.25, c_box_color),
763
+ # ]
764
+ # )
765
+
766
+ third_t.setStyle(TableStyle(third_table_styles))
767
+ # elements.append(platypus.PageBreak("second_page"))
768
+ # elements.append(generate_title(general_data["title"]))
769
+ # elements.append(Spacer(height=LINEHEIGHT, width=0))
770
+ elements.append(third_t)
771
+
772
+ theme_breakdown = ThemeBreakdown(theme_breakdown_df, 8.94 * cm, c_grid_color)
773
+ risk = RiskScale(
774
+ round(general_data["risk_scale"]),
775
+ para_style=s_base_small_justified,
776
+ text=f'This risk ({general_data["risk_scale"]:.1f}) was calculated manually by weighing the risk of all implemented strategies.',
777
+ )
778
+
779
+ max_available_height = CONTENT_HEIGHT - 1.4 * cm - max_height - 0.85 * cm - 0.41 * cm - LINEHEIGHT
780
+ industry_height = get_bar_chart_height(industry)
781
+ left_height = 2 * 0.85 * cm + theme_breakdown.height + risk.height
782
+ right_height = max(min(max_available_height, industry_height), left_height)
783
+
784
+ right_height_diff = right_height - left_height
785
+
786
+ industry_chart = get_bar_chart(industry, 8.94 * cm, right_height - 30)
787
+
788
+ second_charts = [
789
+ [
790
+ Spacer(width=0, height=0),
791
+ Paragraph("<strong>Theme Breakdown and Contribution</strong>", style=s_table_medium_leading),
792
+ Spacer(width=0, height=0),
793
+ Paragraph("<strong>Industry Exposure</strong>", style=s_table_medium_leading),
794
+ ],
795
+ [
796
+ Spacer(width=0, height=0),
797
+ theme_breakdown,
798
+ # Spacer(width=0, height=0),
799
+ # ta_pie_chart,
800
+ Spacer(width=0, height=0),
801
+ # Spacer(width=0, height=0),
802
+ # Spacer(width=0, height=0),
803
+ industry_chart,
804
+ ],
805
+ # [
806
+ # Spacer(width=0, height=0),
807
+ # contrib_bar_chart,
808
+ # Spacer(width=0, height=0),
809
+ # Spacer(width=0, height=0),
810
+ # ],
811
+ [
812
+ Spacer(width=0, height=0),
813
+ Paragraph("<strong>Risk Scale</strong>", style=s_table_medium_leading),
814
+ Spacer(width=0, height=0),
815
+ Spacer(width=0, height=0),
816
+ ],
817
+ [
818
+ Spacer(width=0, height=0),
819
+ risk,
820
+ # 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."),
821
+ # contrib_bar_chart,
822
+ Spacer(width=0, height=0),
823
+ Spacer(width=0, height=0),
824
+ ],
825
+ ]
826
+
827
+ col_width = (CONTENT_WIDTH_PAGE2 - CONTENT_OFFSET - TABLE_MARGIN_PAGE2) / 2
828
+
829
+ other_table = Table(
830
+ second_charts,
831
+ colWidths=[CONTENT_OFFSET, col_width, TABLE_MARGIN_PAGE2, col_width],
832
+ rowHeights=[
833
+ 0.85 * cm,
834
+ theme_breakdown.height,
835
+ # ta_height,
836
+ 0.85 * cm,
837
+ risk.height + right_height_diff,
838
+ ],
839
+ )
840
+
841
+ other_table_styles = [
842
+ ("VALIGN", (1, 1), (1, 1), "TOP"),
843
+ ("SPAN", (3, 1), (3, -1)),
844
+ ("VALIGN", (3, 1), (3, -1), "TOP"),
845
+ ("LINEABOVE", (1, 0), (1, 0), 0.25, c_box_color),
846
+ ("LINEBELOW", (1, 0), (1, 0), 0.25, c_box_color),
847
+ ("LINEABOVE", (3, 0), (3, 0), 0.25, c_box_color),
848
+ ("LINEBELOW", (3, 0), (3, 0), 0.25, c_box_color),
849
+ ("LINEABOVE", (1, 2), (1, 2), 0.25, c_box_color),
850
+ ("LINEBELOW", (1, 2), (1, 2), 0.25, c_box_color),
851
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
852
+ ]
853
+ if debug:
854
+ other_table_styles.extend(
855
+ [
856
+ ("BOX", (0, 0), (-1, -1), 0.25, c_box_color),
857
+ ("INNERGRID", (0, 0), (-1, -1), 0.25, c_box_color),
858
+ ]
859
+ )
860
+
861
+ other_table.setStyle(TableStyle(other_table_styles))
862
+ elements.append(platypus.PageBreak("second_page"))
863
+ elements.append(generate_title(general_data["title"]))
864
+ elements.append(Spacer(height=LINEHEIGHT, width=0))
865
+ elements.append(other_table)
866
+
867
+ impress(elements)
868
+
869
+ doc.build(elements)
870
+ output.seek(0)
871
+
872
+ return output