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